Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions demo/assets/controllers/timeline_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Controller } from '@hotwired/stimulus';
import { getComponent } from '@symfony/ux-live-component';

export default class extends Controller {
async initialize() {
this.component = await getComponent(this.element);
this.scrollToBottom();

const input = document.getElementById('chat-message');
input.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
this.submitMessage();
}
});
input.focus();

const resetButton = document.getElementById('chat-reset');
resetButton.addEventListener('click', (event) => {
this.component.action('reset');
});

const submitButton = document.getElementById('chat-submit');
submitButton.addEventListener('click', (event) => {
this.submitMessage();
});

this.component.on('loading.state:started', (e,r) => {
if (r.actions.includes('reset')) {
return;
}
document.getElementById('welcome')?.remove();
document.getElementById('loading-message').removeAttribute('class');
this.scrollToBottom();
});

this.component.on('loading.state:finished', () => {
document.getElementById('loading-message').setAttribute('class', 'd-none');
});

this.component.on('render:finished', () => {
this.scrollToBottom();
});
};

submitMessage() {
const input = document.getElementById('chat-message');
const message = input.value;
document
.getElementById('loading-message')
.getElementsByClassName('user-message')[0].innerHTML = message;
this.component.action('submit', { message });
input.value = '';
}

scrollToBottom() {
const chatBody = document.getElementById('chat-body');
chatBody.scrollTop = chatBody.scrollHeight;
}
}
4 changes: 4 additions & 0 deletions demo/assets/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,7 @@ body {
}
}
}

.timeline .bot-message img {
max-width: 500px;
}
22 changes: 22 additions & 0 deletions demo/config/packages/ai.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ ai:
api_key: '%env(OPENAI_API_KEY)%'
huggingface:
api_key: '%env(HUGGINGFACE_API_KEY)%'
mcp:
graphify:
transport: sse
url: 'https://agents-mcp-hackathon-graphify.hf.space/gradio_api/mcp/sse'
tools:
- 'Graphify_generate_timeline_diagram'
city:
transport: sse
url: 'https://kingabzpro-live-city-mcp.hf.space/gradio_api/mcp/sse'
tools:
- 'live_city_mcp_get_city_news'
agent:
blog:
platform: 'ai.platform.openai'
Expand Down Expand Up @@ -75,6 +86,17 @@ ai:
model: 'gpt-4o-mini'
prompt: 'You are a helpful general assistant. Assist users with any questions or tasks they may have.'
tools: false
timeline:
platform: 'ai.platform.openai'
model: 'gpt-4o-mini'
prompt: |
You are a news timeline generator. When the user asks about a city:
1) First use live_city_mcp_get_city_news to fetch news for that city
2) Then use Graphify_generate_timeline_diagram with this JSON format:
{"title": "News from [City]", "events_per_row": 3, "events": [{"id": "1", "label": "Short title", "date": "2024-12-13"}]}
tools:
- 'ai.mcp.toolbox.graphify'
- 'ai.mcp.toolbox.city'
multi_agent:
support:
orchestrator: 'orchestrator'
Expand Down
7 changes: 7 additions & 0 deletions demo/config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ youtube:
template: 'chat.html.twig'
context: { chat: 'youtube' }

timeline:
path: '/timeline'
controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController'
defaults:
template: 'chat.html.twig'
context: { chat: 'timeline' }

# Load MCP routes conditionally based on configuration
_mcp:
resource: .
Expand Down
63 changes: 63 additions & 0 deletions demo/src/Timeline/Chat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Timeline;

use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Result\TextResult;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* @author Camille Islasse <camille.islasse@acseo-conseil.fr>
*/
final class Chat
{
private const SESSION_KEY = 'timeline-chat';

public function __construct(
private readonly RequestStack $requestStack,
#[Autowire(service: 'ai.agent.timeline')]
private readonly AgentInterface $agent,
) {
}

public function loadMessages(): MessageBag
{
return $this->requestStack->getSession()->get(self::SESSION_KEY, new MessageBag());
}

public function submitMessage(string $message): void
{
$messages = $this->loadMessages();

$messages->add(Message::ofUser($message));
$result = $this->agent->call($messages);

\assert($result instanceof TextResult);

$messages->add(Message::ofAssistant($result->getContent()));

$this->saveMessages($messages);
}

public function reset(): void
{
$this->requestStack->getSession()->remove(self::SESSION_KEY);
}

private function saveMessages(MessageBag $messages): void
{
$this->requestStack->getSession()->set(self::SESSION_KEY, $messages);
}
}
52 changes: 52 additions & 0 deletions demo/src/Timeline/TwigComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Timeline;

use Symfony\AI\Platform\Message\MessageInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\DefaultActionTrait;

/**
* @author Camille Islasse <camille.islasse@acseo-conseil.fr>
*/
#[AsLiveComponent('timeline')]
final class TwigComponent
{
use DefaultActionTrait;

public function __construct(
private readonly Chat $timeline,
) {
}

/**
* @return MessageInterface[]
*/
public function getMessages(): array
{
return $this->timeline->loadMessages()->withoutSystemMessage()->getMessages();
}

#[LiveAction]
public function submit(#[LiveArg] string $message): void
{
$this->timeline->submitMessage($message);
}

#[LiveAction]
public function reset(): void
{
$this->timeline->reset();
}
}
21 changes: 20 additions & 1 deletion demo/templates/_message.html.twig
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{% if message.role.value == 'assistant' %}
{{ _self.bot(message.content, latest: latest) }}
{% else %}
{% elseif message.role.value == 'tool' and message.hasImageContent() %}
{{ _self.toolResult(message.content) }}
{% elseif message.role.value == 'user' %}
{{ _self.user(message.content) }}
{% endif %}

Expand Down Expand Up @@ -51,3 +53,20 @@
</div>
</div>
{% endmacro %}

{% macro toolResult(content) %}
<div class="d-flex align-items-baseline mb-4">
<div class="tool avatar rounded-3 shadow-sm bg-secondary-subtle">
{{ ux_icon('mdi:tools', { height: '45px', width: '45px' }) }}
</div>
<div class="ps-2">
{% for item in content %}
{% if item.format is defined and item.format starts with 'image/' %}
<div class="tool-result d-inline-block p-2 m-1 border border-light-subtle shadow-sm">
<img src="{{ item.asDataUrl() }}" class="img-fluid rounded" style="max-width: 500px;">
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endmacro %}
30 changes: 30 additions & 0 deletions demo/templates/components/timeline.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% import "_message.html.twig" as message %}

<div class="card mx-auto shadow-lg" {{ attributes.defaults(stimulus_controller('timeline')) }}>
<div class="card-header p-2">
{{ ux_icon('mdi:timeline', { height: '32px', width: '32px' }) }}
<strong class="ms-1 pt-1 d-inline-block">Timeline Bot</strong>
<button id="chat-reset" class="btn btn-sm btn-outline-secondary float-end">{{ ux_icon('material-symbols:cancel') }} Reset Chat</button>
</div>
<div id="chat-body" class="card-body p-4 overflow-auto">
{% for message in this.messages %}
{% include '_message.html.twig' with { message, latest: loop.last } %}
{% else %}
<div id="welcome" class="text-center mt-5 py-5 bg-white rounded-5 shadow-sm w-75 mx-auto">
{{ ux_icon('mdi:timeline', { height: '200px', width: '200px' }) }}
<h4 class="mt-5">Generate news timelines with AI using MCP</h4>
<span class="text-muted">Try: "Show me the latest news from Berlin"</span>
</div>
{% endfor %}
<div id="loading-message" class="d-none">
{{ message.user([{text:''}]) }}
{{ message.bot('The Timeline Bot is generating your timeline ...', true) }}
</div>
</div>
<div class="card-footer p-2">
<div class="input-group">
<input id="chat-message" type="text" class="form-control border-0" placeholder="Write a message ...">
<button id="chat-submit" class="btn btn-outline-secondary border-0" type="button">{{ ux_icon('mingcute:send-fill', { height: '25px', width: '25px' }) }} Submit</button>
</div>
</div>
</div>
21 changes: 21 additions & 0 deletions demo/templates/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,26 @@
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-3">
<div class="card timeline bg-body shadow-sm">
<div class="card-img-top py-2">
{{ ux_icon('mdi:timeline', { height: '150px', width: '150px' }) }}
</div>
<div class="card-body">
<h5 class="card-title">Timeline Bot</h5>
<p class="card-text">Generate news timelines using MCP client integration.</p>
<a href="{{ path('timeline') }}" class="btn btn-outline-dark d-block">Try Timeline Bot</a>
</div>
{# Profiler route only available in dev #}
{% if 'dev' == app.environment %}
<div class="card-footer">
{{ ux_icon('solar:code-linear', { height: '20px', width: '20px' }) }}
<a href="{{ path('_profiler_open_file', { file: 'src/Timeline/Chat.php', line: 24 }) }}">See Implementation</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
1 change: 1 addition & 0 deletions splitsh.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"ai-tavily-tool": "src/agent/src/Bridge/Tavily",
"ai-youtube-tool": "src/agent/src/Bridge/Youtube",
"ai-wikipedia-tool": "src/agent/src/Bridge/Wikipedia",
"ai-mcp-tool": "src/agent/src/Bridge/Mcp",
"ai-bundle": "src/ai-bundle",
"ai-chat": "src/chat",
"mcp-bundle": "src/mcp-bundle",
Expand Down
3 changes: 3 additions & 0 deletions src/agent/src/Bridge/Mcp/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.git* export-ignore
8 changes: 8 additions & 0 deletions src/agent/src/Bridge/Mcp/.github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Please do not submit any Pull Requests here. They will be closed.
---

Please submit your PR here instead:
https://github.com/symfony/ai

This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
20 changes: 20 additions & 0 deletions src/agent/src/Bridge/Mcp/.github/close-pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Close Pull Request

on:
pull_request_target:
types: [opened]

jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.

However, you should instead open your PR on the main repository:
https://github.com/symfony/ai

This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
5 changes: 5 additions & 0 deletions src/agent/src/Bridge/Mcp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
vendor/
composer.lock
phpunit.xml
.phpunit.result.cache
.phpunit.cache/
9 changes: 9 additions & 0 deletions src/agent/src/Bridge/Mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CHANGELOG
=========

0.1
---

* Initial release with MCP client support
* HTTP, SSE, and Stdio transports
* McpToolbox implementing ToolboxInterface
Loading
Loading