Skip to content

Commit ff56586

Browse files
committed
feat(aibundle): improve support for traceable*
1 parent 7da897a commit ff56586

File tree

14 files changed

+500
-5
lines changed

14 files changed

+500
-5
lines changed

docs/bundles/ai-bundle.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,48 @@ The profiler panel provides insights into the agent's execution:
950950
.. image:: profiler.png
951951
:alt: Profiler Panel
952952

953+
Message stores
954+
--------------
955+
956+
Message stores are critical to store messages sent to agents in the short / long term, they can be configured
957+
and reused in multiple chats, providing the capacity to agents to keep previous interactions.
958+
959+
Configuring message stores
960+
~~~~~~~~~~~~~~~~~~~~~~~~~~
961+
962+
Message stores are defined in the ``message_store`` section of your configuration:
963+
964+
.. code-block:: yaml
965+
966+
ai:
967+
# ...
968+
message_store:
969+
youtube:
970+
cache:
971+
service: 'cache.app'
972+
key: 'youtube'
973+
974+
Chats
975+
-----
976+
977+
Chats are the entrypoint when it comes to sending messages to agents and retrieving content (mostly text)
978+
that contains the response from the agent.
979+
980+
Each chat requires to define an agent and a message store.
981+
982+
Configuring Chats
983+
~~~~~~~~~~~~~~~~~
984+
985+
Chats are defined in the ``chat`` section of your configuration:
986+
987+
.. code-block:: yaml
988+
989+
ai:
990+
# ...
991+
chat:
992+
youtube:
993+
agent: 'ai.agent.youtube'
994+
message_store: 'ai.message_store.cache.youtube'
953995
954996
.. _`Symfony AI Agent`: https://github.com/symfony/ai-agent
955997
.. _`Symfony AI Chat`: https://github.com/symfony/ai-chat

src/ai-bundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ CHANGELOG
3333
- Automatic registration of token output processors for Mistral, OpenAI and Vertex AI
3434
- Token usage metadata in agent results including prompt, completion, total, cached, and thinking tokens
3535
- Rate limit information tracking for supported platforms
36+
* Add support for configuring chats and message stores

src/ai-bundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"symfony/ai-chat": "@dev",
2020
"symfony/ai-platform": "@dev",
2121
"symfony/ai-store": "@dev",
22+
"symfony/clock": "^7.3|^8.0",
2223
"symfony/config": "^7.3|^8.0",
2324
"symfony/console": "^7.3|^8.0",
2425
"symfony/dependency-injection": "^7.3|^8.0",
@@ -30,7 +31,6 @@
3031
"phpstan/phpstan": "^2.1",
3132
"phpstan/phpstan-strict-rules": "^2.0",
3233
"phpunit/phpunit": "^11.5",
33-
"symfony/clock": "^7.3|^8.0",
3434
"symfony/expression-language": "^7.3|^8.0",
3535
"symfony/security-core": "^7.3|^8.0",
3636
"symfony/translation": "^7.3|^8.0"

src/ai-bundle/config/options.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,15 @@
774774
->end()
775775
->end()
776776
->end()
777+
->arrayNode('chat')
778+
->useAttributeAsKey('name')
779+
->arrayPrototype()
780+
->children()
781+
->stringNode('agent')->cannotBeEmpty()->end()
782+
->stringNode('message_store')->cannotBeEmpty()->end()
783+
->end()
784+
->end()
785+
->end()
777786
->arrayNode('vectorizer')
778787
->info('Vectorizers for converting strings to Vector objects and transforming TextDocument arrays to VectorDocument arrays')
779788
->useAttributeAsKey('name')

src/ai-bundle/config/services.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@
168168
->args([
169169
tagged_iterator('ai.traceable_platform'),
170170
tagged_iterator('ai.traceable_toolbox'),
171+
tagged_iterator('ai.traceable_message_store'),
172+
tagged_iterator('ai.traceable_chat'),
171173
])
172174
->tag('data_collector')
173175

src/ai-bundle/src/AiBundle.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@
3131
use Symfony\AI\Agent\Toolbox\ToolFactory\MemoryToolFactory;
3232
use Symfony\AI\AiBundle\DependencyInjection\ProcessorCompilerPass;
3333
use Symfony\AI\AiBundle\Exception\InvalidArgumentException;
34+
use Symfony\AI\AiBundle\Profiler\TraceableChat;
35+
use Symfony\AI\AiBundle\Profiler\TraceableMessageStore;
3436
use Symfony\AI\AiBundle\Profiler\TraceablePlatform;
3537
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
3638
use Symfony\AI\AiBundle\Security\Attribute\IsGrantedTool;
3739
use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore;
3840
use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore as MeilisearchMessageStore;
3941
use Symfony\AI\Chat\Bridge\Pogocache\MessageStore as PogocacheMessageStore;
42+
use Symfony\AI\Chat\Chat;
43+
use Symfony\AI\Chat\ChatInterface;
4044
use Symfony\AI\Chat\MessageStoreInterface;
4145
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
4246
use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory;
@@ -179,11 +183,49 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
179183
$builder->setAlias(MessageStoreInterface::class, reset($messageStores));
180184
}
181185

186+
if ($builder->getParameter('kernel.debug')) {
187+
foreach ($messageStores as $messageStore) {
188+
$traceableMessageStoreDefinition = (new Definition(TraceableMessageStore::class))
189+
->setDecoratedService($messageStore)
190+
->setArguments([
191+
new Reference('.inner'),
192+
new Reference(ClockInterface::class),
193+
])
194+
->addTag('ai.traceable_message_store');
195+
$suffix = u($messageStore)->afterLast('.')->toString();
196+
$builder->setDefinition('ai.traceable_message_store.'.$suffix, $traceableMessageStoreDefinition);
197+
}
198+
}
199+
182200
if ([] === $messageStores) {
183201
$builder->removeDefinition('ai.command.setup_message_store');
184202
$builder->removeDefinition('ai.command.drop_message_store');
185203
}
186204

205+
foreach ($config['chat'] ?? [] as $name => $chat) {
206+
$this->processChatConfig($name, $chat, $builder);
207+
}
208+
209+
$chats = array_keys($builder->findTaggedServiceIds('ai.chat'));
210+
211+
if (1 === \count($chats)) {
212+
$builder->setAlias(ChatInterface::class, reset($chats));
213+
}
214+
215+
if ($builder->getParameter('kernel.debug')) {
216+
foreach ($chats as $chat) {
217+
$traceableChatDefinition = (new Definition(TraceableChat::class))
218+
->setDecoratedService($chat)
219+
->setArguments([
220+
new Reference('.inner'),
221+
new Reference(ClockInterface::class),
222+
])
223+
->addTag('ai.traceable_chat');
224+
$suffix = u($chat)->afterLast('.')->toString();
225+
$builder->setDefinition('ai.traceable_chat.'.$suffix, $traceableChatDefinition);
226+
}
227+
}
228+
187229
foreach ($config['vectorizer'] ?? [] as $vectorizerName => $vectorizer) {
188230
$this->processVectorizerConfig($vectorizerName, $vectorizer, $builder);
189231
}
@@ -1396,6 +1438,26 @@ private function processMessageStoreConfig(string $type, array $messageStores, C
13961438
}
13971439
}
13981440

1441+
/**
1442+
* @param array{
1443+
* agent: string,
1444+
* message_store: string,
1445+
* } $configuration
1446+
*/
1447+
private function processChatConfig(string $name, array $configuration, ContainerBuilder $container): void
1448+
{
1449+
$definition = new Definition(Chat::class);
1450+
$definition
1451+
->setArguments([
1452+
new Reference($configuration['agent']),
1453+
new Reference($configuration['message_store']),
1454+
])
1455+
->addTag('ai.chat');
1456+
1457+
$container->setDefinition('ai.chat.'.$name, $definition);
1458+
$container->registerAliasForArgument('ai.chat.'.$name, ChatInterface::class, $name);
1459+
}
1460+
13991461
/**
14001462
* @param array<string, mixed> $config
14011463
*/

src/ai-bundle/src/Profiler/DataCollector.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
* @author Christopher Hertel <[email protected]>
2424
*
2525
* @phpstan-import-type PlatformCallData from TraceablePlatform
26+
* @phpstan-import-type MessageStoreData from TraceableMessageStore
27+
* @phpstan-import-type ChatData from TraceableChat
2628
*/
2729
final class DataCollector extends AbstractDataCollector implements LateDataCollectorInterface
2830
{
@@ -37,15 +39,31 @@ final class DataCollector extends AbstractDataCollector implements LateDataColle
3739
private readonly array $toolboxes;
3840

3941
/**
40-
* @param TraceablePlatform[] $platforms
41-
* @param TraceableToolbox[] $toolboxes
42+
* @var TraceableMessageStore[]
43+
*/
44+
private readonly array $messageStores;
45+
46+
/**
47+
* @var TraceableChat[]
48+
*/
49+
private readonly array $chats;
50+
51+
/**
52+
* @param TraceablePlatform[] $platforms
53+
* @param TraceableToolbox[] $toolboxes
54+
* @param TraceableMessageStore[] $messageStores
55+
* @param TraceableChat[] $chats
4256
*/
4357
public function __construct(
4458
iterable $platforms,
4559
iterable $toolboxes,
60+
iterable $messageStores,
61+
iterable $chats,
4662
) {
4763
$this->platforms = $platforms instanceof \Traversable ? iterator_to_array($platforms) : $platforms;
4864
$this->toolboxes = $toolboxes instanceof \Traversable ? iterator_to_array($toolboxes) : $toolboxes;
65+
$this->messageStores = $messageStores instanceof \Traversable ? iterator_to_array($messageStores) : $messageStores;
66+
$this->chats = $chats instanceof \Traversable ? iterator_to_array($chats) : $chats;
4967
}
5068

5169
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
@@ -59,6 +77,8 @@ public function lateCollect(): void
5977
'tools' => $this->getAllTools(),
6078
'platform_calls' => array_merge(...array_map($this->awaitCallResults(...), $this->platforms)),
6179
'tool_calls' => array_merge(...array_map(fn (TraceableToolbox $toolbox) => $toolbox->calls, $this->toolboxes)),
80+
'messages' => array_merge(...array_map(static fn (TraceableMessageStore $messageStore): array => $messageStore->calls, $this->messageStores)),
81+
'chats' => array_merge(...array_map(static fn (TraceableChat $chat): array => $chat->calls, $this->chats)),
6282
];
6383
}
6484

@@ -91,6 +111,22 @@ public function getToolCalls(): array
91111
return $this->data['tool_calls'] ?? [];
92112
}
93113

114+
/**
115+
* @return MessageStoreData[]
116+
*/
117+
public function getMessages(): array
118+
{
119+
return $this->data['messages'] ?? [];
120+
}
121+
122+
/**
123+
* @return ChatData[]
124+
*/
125+
public function getChats(): array
126+
{
127+
return $this->data['chats'] ?? [];
128+
}
129+
94130
/**
95131
* @return Tool[]
96132
*/
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 Symfony\AI\AiBundle\Profiler;
13+
14+
use Symfony\AI\Chat\ChatInterface;
15+
use Symfony\AI\Platform\Message\AssistantMessage;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
use Symfony\AI\Platform\Message\UserMessage;
18+
use Symfony\Component\Clock\ClockInterface;
19+
20+
/**
21+
* @author Guillaume Loulier <[email protected]>
22+
*
23+
* @phpstan-type ChatData array{
24+
* action: string,
25+
* bag?: MessageBag,
26+
* message?: UserMessage,
27+
* saved_at: \DateTimeImmutable,
28+
* }
29+
*/
30+
final class TraceableChat implements ChatInterface
31+
{
32+
/**
33+
* @var array<int, array{
34+
* action: string,
35+
* bag?: MessageBag,
36+
* message?: UserMessage,
37+
* saved_at: \DateTimeImmutable,
38+
* }>
39+
*/
40+
public array $calls = [];
41+
42+
public function __construct(
43+
private readonly ChatInterface $chat,
44+
private readonly ClockInterface $clock,
45+
) {
46+
}
47+
48+
public function initiate(MessageBag $messages): void
49+
{
50+
$this->calls[] = [
51+
'action' => __FUNCTION__,
52+
'bag' => $messages,
53+
'saved_at' => $this->clock->now(),
54+
];
55+
56+
$this->chat->initiate($messages);
57+
}
58+
59+
public function submit(UserMessage $message): AssistantMessage
60+
{
61+
$this->calls[] = [
62+
'action' => __FUNCTION__,
63+
'message' => $message,
64+
'saved_at' => $this->clock->now(),
65+
];
66+
67+
return $this->chat->submit($message);
68+
}
69+
}

0 commit comments

Comments
 (0)