Skip to content

Commit 9f13e8d

Browse files
[Agent] Enable remote MCP tools for agents
1 parent 150db7e commit 9f13e8d

File tree

53 files changed

+2723
-54
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2723
-54
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
import { getComponent } from '@symfony/ux-live-component';
3+
4+
export default class extends Controller {
5+
async initialize() {
6+
this.component = await getComponent(this.element);
7+
this.scrollToBottom();
8+
9+
const input = document.getElementById('chat-message');
10+
input.addEventListener('keypress', (event) => {
11+
if (event.key === 'Enter') {
12+
this.submitMessage();
13+
}
14+
});
15+
input.focus();
16+
17+
const resetButton = document.getElementById('chat-reset');
18+
resetButton.addEventListener('click', (event) => {
19+
this.component.action('reset');
20+
});
21+
22+
const submitButton = document.getElementById('chat-submit');
23+
submitButton.addEventListener('click', (event) => {
24+
this.submitMessage();
25+
});
26+
27+
this.component.on('loading.state:started', (e,r) => {
28+
if (r.actions.includes('reset')) {
29+
return;
30+
}
31+
document.getElementById('welcome')?.remove();
32+
document.getElementById('loading-message').removeAttribute('class');
33+
this.scrollToBottom();
34+
});
35+
36+
this.component.on('loading.state:finished', () => {
37+
document.getElementById('loading-message').setAttribute('class', 'd-none');
38+
});
39+
40+
this.component.on('render:finished', () => {
41+
this.scrollToBottom();
42+
});
43+
};
44+
45+
submitMessage() {
46+
const input = document.getElementById('chat-message');
47+
const message = input.value;
48+
document
49+
.getElementById('loading-message')
50+
.getElementsByClassName('user-message')[0].innerHTML = message;
51+
this.component.action('submit', { message });
52+
input.value = '';
53+
}
54+
55+
scrollToBottom() {
56+
const chatBody = document.getElementById('chat-body');
57+
chatBody.scrollTop = chatBody.scrollHeight;
58+
}
59+
}

demo/assets/styles/app.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,7 @@ body {
6363
}
6464
}
6565
}
66+
67+
.timeline .bot-message img {
68+
max-width: 500px;
69+
}

demo/config/packages/ai.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ ai:
44
api_key: '%env(OPENAI_API_KEY)%'
55
huggingface:
66
api_key: '%env(HUGGINGFACE_API_KEY)%'
7+
mcp:
8+
graphify:
9+
transport: sse
10+
url: 'https://agents-mcp-hackathon-graphify.hf.space/gradio_api/mcp/sse'
11+
tools:
12+
- 'Graphify_generate_timeline_diagram'
13+
city:
14+
transport: sse
15+
url: 'https://kingabzpro-live-city-mcp.hf.space/gradio_api/mcp/sse'
16+
tools:
17+
- 'live_city_mcp_get_city_news'
718
agent:
819
blog:
920
platform: 'ai.platform.openai'
@@ -75,6 +86,17 @@ ai:
7586
model: 'gpt-4o-mini'
7687
prompt: 'You are a helpful general assistant. Assist users with any questions or tasks they may have.'
7788
tools: false
89+
timeline:
90+
platform: 'ai.platform.openai'
91+
model: 'gpt-4o-mini'
92+
prompt: |
93+
You are a news timeline generator. When the user asks about a city:
94+
1) First use live_city_mcp_get_city_news to fetch news for that city
95+
2) Then use Graphify_generate_timeline_diagram with this JSON format:
96+
{"title": "News from [City]", "events_per_row": 3, "events": [{"id": "1", "label": "Short title", "date": "2024-12-13"}]}
97+
tools:
98+
- 'ai.mcp.toolbox.graphify'
99+
- 'ai.mcp.toolbox.city'
78100
multi_agent:
79101
support:
80102
orchestrator: 'orchestrator'

demo/config/routes.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ youtube:
6363
template: 'chat.html.twig'
6464
context: { chat: 'youtube' }
6565

66+
timeline:
67+
path: '/timeline'
68+
controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController'
69+
defaults:
70+
template: 'chat.html.twig'
71+
context: { chat: 'timeline' }
72+
6673
# Load MCP routes conditionally based on configuration
6774
_mcp:
6875
resource: .

demo/src/Timeline/Chat.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace App\Timeline;
13+
14+
use Symfony\AI\Agent\AgentInterface;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
use Symfony\AI\Platform\Result\TextResult;
18+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
19+
use Symfony\Component\HttpFoundation\RequestStack;
20+
21+
/**
22+
* @author Camille Islasse <[email protected]>
23+
*/
24+
final class Chat
25+
{
26+
private const SESSION_KEY = 'timeline-chat';
27+
28+
public function __construct(
29+
private readonly RequestStack $requestStack,
30+
#[Autowire(service: 'ai.agent.timeline')]
31+
private readonly AgentInterface $agent,
32+
) {
33+
}
34+
35+
public function loadMessages(): MessageBag
36+
{
37+
return $this->requestStack->getSession()->get(self::SESSION_KEY, new MessageBag());
38+
}
39+
40+
public function submitMessage(string $message): void
41+
{
42+
$messages = $this->loadMessages();
43+
44+
$messages->add(Message::ofUser($message));
45+
$result = $this->agent->call($messages);
46+
47+
\assert($result instanceof TextResult);
48+
49+
$messages->add(Message::ofAssistant($result->getContent()));
50+
51+
$this->saveMessages($messages);
52+
}
53+
54+
public function reset(): void
55+
{
56+
$this->requestStack->getSession()->remove(self::SESSION_KEY);
57+
}
58+
59+
private function saveMessages(MessageBag $messages): void
60+
{
61+
$this->requestStack->getSession()->set(self::SESSION_KEY, $messages);
62+
}
63+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace App\Timeline;
13+
14+
use Symfony\AI\Platform\Message\MessageInterface;
15+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
16+
use Symfony\UX\LiveComponent\Attribute\LiveAction;
17+
use Symfony\UX\LiveComponent\Attribute\LiveArg;
18+
use Symfony\UX\LiveComponent\DefaultActionTrait;
19+
20+
/**
21+
* @author Camille Islasse <[email protected]>
22+
*/
23+
#[AsLiveComponent('timeline')]
24+
final class TwigComponent
25+
{
26+
use DefaultActionTrait;
27+
28+
public function __construct(
29+
private readonly Chat $timeline,
30+
) {
31+
}
32+
33+
/**
34+
* @return MessageInterface[]
35+
*/
36+
public function getMessages(): array
37+
{
38+
return $this->timeline->loadMessages()->withoutSystemMessage()->getMessages();
39+
}
40+
41+
#[LiveAction]
42+
public function submit(#[LiveArg] string $message): void
43+
{
44+
$this->timeline->submitMessage($message);
45+
}
46+
47+
#[LiveAction]
48+
public function reset(): void
49+
{
50+
$this->timeline->reset();
51+
}
52+
}

demo/templates/_message.html.twig

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{% if message.role.value == 'assistant' %}
22
{{ _self.bot(message.content, latest: latest) }}
3-
{% else %}
3+
{% elseif message.role.value == 'tool' and message.hasImageContent() %}
4+
{{ _self.toolResult(message.content) }}
5+
{% elseif message.role.value == 'user' %}
46
{{ _self.user(message.content) }}
57
{% endif %}
68

@@ -51,3 +53,20 @@
5153
</div>
5254
</div>
5355
{% endmacro %}
56+
57+
{% macro toolResult(content) %}
58+
<div class="d-flex align-items-baseline mb-4">
59+
<div class="tool avatar rounded-3 shadow-sm bg-secondary-subtle">
60+
{{ ux_icon('mdi:tools', { height: '45px', width: '45px' }) }}
61+
</div>
62+
<div class="ps-2">
63+
{% for item in content %}
64+
{% if item.format is defined and item.format starts with 'image/' %}
65+
<div class="tool-result d-inline-block p-2 m-1 border border-light-subtle shadow-sm">
66+
<img src="{{ item.asDataUrl() }}" class="img-fluid rounded" style="max-width: 500px;">
67+
</div>
68+
{% endif %}
69+
{% endfor %}
70+
</div>
71+
</div>
72+
{% endmacro %}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{% import "_message.html.twig" as message %}
2+
3+
<div class="card mx-auto shadow-lg" {{ attributes.defaults(stimulus_controller('timeline')) }}>
4+
<div class="card-header p-2">
5+
{{ ux_icon('mdi:timeline', { height: '32px', width: '32px' }) }}
6+
<strong class="ms-1 pt-1 d-inline-block">Timeline Bot</strong>
7+
<button id="chat-reset" class="btn btn-sm btn-outline-secondary float-end">{{ ux_icon('material-symbols:cancel') }} Reset Chat</button>
8+
</div>
9+
<div id="chat-body" class="card-body p-4 overflow-auto">
10+
{% for message in this.messages %}
11+
{% include '_message.html.twig' with { message, latest: loop.last } %}
12+
{% else %}
13+
<div id="welcome" class="text-center mt-5 py-5 bg-white rounded-5 shadow-sm w-75 mx-auto">
14+
{{ ux_icon('mdi:timeline', { height: '200px', width: '200px' }) }}
15+
<h4 class="mt-5">Generate news timelines with AI using MCP</h4>
16+
<span class="text-muted">Try: "Show me the latest news from Berlin"</span>
17+
</div>
18+
{% endfor %}
19+
<div id="loading-message" class="d-none">
20+
{{ message.user([{text:''}]) }}
21+
{{ message.bot('The Timeline Bot is generating your timeline ...', true) }}
22+
</div>
23+
</div>
24+
<div class="card-footer p-2">
25+
<div class="input-group">
26+
<input id="chat-message" type="text" class="form-control border-0" placeholder="Write a message ...">
27+
<button id="chat-submit" class="btn btn-outline-secondary border-0" type="button">{{ ux_icon('mingcute:send-fill', { height: '25px', width: '25px' }) }} Submit</button>
28+
</div>
29+
</div>
30+
</div>

demo/templates/index.html.twig

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,5 +168,26 @@
168168
</div>
169169
</div>
170170
</div>
171+
<div class="row mt-4">
172+
<div class="col-md-3">
173+
<div class="card timeline bg-body shadow-sm">
174+
<div class="card-img-top py-2">
175+
{{ ux_icon('mdi:timeline', { height: '150px', width: '150px' }) }}
176+
</div>
177+
<div class="card-body">
178+
<h5 class="card-title">Timeline Bot</h5>
179+
<p class="card-text">Generate news timelines using MCP client integration.</p>
180+
<a href="{{ path('timeline') }}" class="btn btn-outline-dark d-block">Try Timeline Bot</a>
181+
</div>
182+
{# Profiler route only available in dev #}
183+
{% if 'dev' == app.environment %}
184+
<div class="card-footer">
185+
{{ ux_icon('solar:code-linear', { height: '20px', width: '20px' }) }}
186+
<a href="{{ path('_profiler_open_file', { file: 'src/Timeline/Chat.php', line: 24 }) }}">See Implementation</a>
187+
</div>
188+
{% endif %}
189+
</div>
190+
</div>
191+
</div>
171192
</div>
172193
{% endblock %}

splitsh.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"ai-tavily-tool": "src/agent/src/Bridge/Tavily",
1616
"ai-youtube-tool": "src/agent/src/Bridge/Youtube",
1717
"ai-wikipedia-tool": "src/agent/src/Bridge/Wikipedia",
18+
"ai-mcp-tool": "src/agent/src/Bridge/Mcp",
1819
"ai-bundle": "src/ai-bundle",
1920
"ai-chat": "src/chat",
2021
"mcp-bundle": "src/mcp-bundle",

0 commit comments

Comments
 (0)