Skip to content

Commit b20fb42

Browse files
committed
feat(core): commands added for message store + configuration
1 parent ec39514 commit b20fb42

File tree

15 files changed

+703
-5
lines changed

15 files changed

+703
-5
lines changed

src/ai-bundle/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"require": {
1717
"php": ">=8.2",
1818
"symfony/ai-agent": "@dev",
19+
"symfony/ai-chat": "@dev",
1920
"symfony/ai-platform": "@dev",
2021
"symfony/ai-store": "@dev",
2122
"symfony/config": "^7.3|^8.0",

src/ai-bundle/config/options.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,35 @@
670670
->end()
671671
->end()
672672
->end()
673+
->arrayNode('message_store')
674+
->children()
675+
->arrayNode('memory')
676+
->useAttributeAsKey('name')
677+
->arrayPrototype()
678+
->children()
679+
->stringNode('identifier')->cannotBeEmpty()->end()
680+
->end()
681+
->end()
682+
->end()
683+
->arrayNode('cache')
684+
->useAttributeAsKey('name')
685+
->arrayPrototype()
686+
->children()
687+
->stringNode('service')->cannotBeEmpty()->defaultValue('cache.app')->end()
688+
->stringNode('key')->end()
689+
->end()
690+
->end()
691+
->end()
692+
->arrayNode('session')
693+
->useAttributeAsKey('name')
694+
->arrayPrototype()
695+
->children()
696+
->stringNode('identifier')->cannotBeEmpty()->end()
697+
->end()
698+
->end()
699+
->end()
700+
->end()
701+
->end()
673702
->arrayNode('vectorizer')
674703
->info('Vectorizers for converting strings to Vector objects and transforming TextDocument arrays to VectorDocument arrays')
675704
->useAttributeAsKey('name')

src/ai-bundle/config/services.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
use Symfony\AI\AiBundle\Profiler\DataCollector;
2626
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
2727
use Symfony\AI\AiBundle\Security\EventListener\IsGrantedToolAttributeListener;
28+
use Symfony\AI\Chat\Command\DropStoreCommand as DropMessageStoreCommand;
29+
use Symfony\AI\Chat\Command\SetupStoreCommand as SetupMessageStoreCommand;
2830
use Symfony\AI\Platform\Bridge\AiMlApi\ModelCatalog as AiMlApiModelCatalog;
2931
use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract;
3032
use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog;
@@ -216,5 +218,15 @@
216218
tagged_locator('ai.indexer', 'name'),
217219
])
218220
->tag('console.command')
221+
->set('ai.command.setup_message_store', SetupMessageStoreCommand::class)
222+
->args([
223+
tagged_locator('ai.message_store', 'name'),
224+
])
225+
->tag('console.command')
226+
->set('ai.command.drop_message_store', DropMessageStoreCommand::class)
227+
->args([
228+
tagged_locator('ai.message_store', 'name'),
229+
])
230+
->tag('console.command')
219231
;
220232
};

src/ai-bundle/src/AiBundle.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
use Symfony\AI\AiBundle\Profiler\TraceablePlatform;
3535
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
3636
use Symfony\AI\AiBundle\Security\Attribute\IsGrantedTool;
37+
use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore;
38+
use Symfony\AI\Chat\MessageStoreInterface;
3739
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
3840
use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory;
3941
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory as CerebrasPlatformFactory;
@@ -161,6 +163,21 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
161163
$builder->removeDefinition('ai.command.drop_store');
162164
}
163165

166+
foreach ($config['message_store'] ?? [] as $type => $store) {
167+
$this->processMessageStoreConfig($type, $store, $builder);
168+
}
169+
170+
$messageStores = array_keys($builder->findTaggedServiceIds('ai.message_store'));
171+
172+
if (1 === \count($messageStores)) {
173+
$builder->setAlias(MessageStoreInterface::class, reset($messageStores));
174+
}
175+
176+
if ([] === $messageStores) {
177+
$builder->removeDefinition('ai.command.setup_message_store');
178+
$builder->removeDefinition('ai.command.drop_message_store');
179+
}
180+
164181
foreach ($config['vectorizer'] ?? [] as $vectorizerName => $vectorizer) {
165182
$this->processVectorizerConfig($vectorizerName, $vectorizer, $builder);
166183
}
@@ -1191,6 +1208,62 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde
11911208
}
11921209
}
11931210

1211+
/**
1212+
* @param array<string, mixed> $messageStores
1213+
*/
1214+
private function processMessageStoreConfig(string $type, array $messageStores, ContainerBuilder $container): void
1215+
{
1216+
if ('memory' === $type) {
1217+
foreach ($messageStores as $name => $messageStore) {
1218+
$definition = new Definition(InMemoryStore::class);
1219+
$definition
1220+
->setArgument(0, $messageStore['identifier'])
1221+
->addTag('ai.message_store');
1222+
1223+
$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
1224+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
1225+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
1226+
}
1227+
}
1228+
1229+
if ('cache' === $type) {
1230+
foreach ($messageStores as $name => $messageStore) {
1231+
$arguments = [
1232+
new Reference($messageStore['service']),
1233+
];
1234+
1235+
if (\array_key_exists('key', $messageStore)) {
1236+
$arguments['key'] = $messageStore['key'];
1237+
}
1238+
1239+
$definition = new Definition(CacheStore::class);
1240+
$definition
1241+
->setArguments($arguments)
1242+
->addTag('ai.message_store');
1243+
1244+
$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
1245+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
1246+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
1247+
}
1248+
}
1249+
1250+
if ('session' === $type) {
1251+
foreach ($messageStores as $name => $messageStore) {
1252+
$definition = new Definition(SessionStore::class);
1253+
$definition
1254+
->setArguments([
1255+
new Reference('request_stack'),
1256+
$messageStore['identifier'],
1257+
])
1258+
->addTag('ai.message_store');
1259+
1260+
$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
1261+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
1262+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
1263+
}
1264+
}
1265+
}
1266+
11941267
/**
11951268
* @param array<string, mixed> $config
11961269
*/

src/ai-bundle/tests/DependencyInjection/AiBundleTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\AI\Agent\MultiAgent\Handoff;
2222
use Symfony\AI\Agent\MultiAgent\MultiAgent;
2323
use Symfony\AI\AiBundle\AiBundle;
24+
use Symfony\AI\Chat\MessageStoreInterface;
2425
use Symfony\AI\Store\Document\Filter\TextContainsFilter;
2526
use Symfony\AI\Store\Document\Loader\InMemoryLoader;
2627
use Symfony\AI\Store\Document\Transformer\TextTrimTransformer;
@@ -58,6 +59,30 @@ public function testStoreCommandsArentDefinedWithoutStore()
5859
$this->assertSame([
5960
'ai.command.setup_store' => true,
6061
'ai.command.drop_store' => true,
62+
'ai.command.setup_message_store' => true,
63+
'ai.command.drop_message_store' => true,
64+
], $container->getRemovedIds());
65+
}
66+
67+
public function testMessageStoreCommandsArentDefinedWithoutMessageStore()
68+
{
69+
$container = $this->buildContainer([
70+
'ai' => [
71+
'agent' => [
72+
'my_agent' => [
73+
'model' => 'gpt-4',
74+
],
75+
],
76+
],
77+
]);
78+
79+
$this->assertFalse($container->hasDefinition('ai.command.setup_message_store'));
80+
$this->assertFalse($container->hasDefinition('ai.command.drop_message_store'));
81+
$this->assertSame([
82+
'ai.command.setup_store' => true,
83+
'ai.command.drop_store' => true,
84+
'ai.command.setup_message_store' => true,
85+
'ai.command.drop_message_store' => true,
6186
], $container->getRemovedIds());
6287
}
6388

@@ -78,6 +103,23 @@ public function testStoreCommandsAreDefined()
78103
$this->assertArrayHasKey('console.command', $dropStoreCommandDefinition->getTags());
79104
}
80105

106+
public function testMessageStoreCommandsAreDefined()
107+
{
108+
$container = $this->buildContainer($this->getFullConfig());
109+
110+
$this->assertTrue($container->hasDefinition('ai.command.setup_message_store'));
111+
112+
$setupStoreCommandDefinition = $container->getDefinition('ai.command.setup_message_store');
113+
$this->assertCount(1, $setupStoreCommandDefinition->getArguments());
114+
$this->assertArrayHasKey('console.command', $setupStoreCommandDefinition->getTags());
115+
116+
$this->assertTrue($container->hasDefinition('ai.command.drop_message_store'));
117+
118+
$dropStoreCommandDefinition = $container->getDefinition('ai.command.drop_message_store');
119+
$this->assertCount(1, $dropStoreCommandDefinition->getArguments());
120+
$this->assertArrayHasKey('console.command', $dropStoreCommandDefinition->getTags());
121+
}
122+
81123
public function testInjectionAgentAliasIsRegistered()
82124
{
83125
$container = $this->buildContainer([
@@ -125,6 +167,31 @@ public function testInjectionStoreAliasIsRegistered()
125167
$this->assertTrue($container->hasAlias(StoreInterface::class.' $weaviateMain'));
126168
}
127169

170+
public function testInjectionMessageStoreAliasIsRegistered()
171+
{
172+
$container = $this->buildContainer([
173+
'ai' => [
174+
'message_store' => [
175+
'memory' => [
176+
'main' => [
177+
'identifier' => '_memory',
178+
],
179+
],
180+
'session' => [
181+
'session' => [
182+
'identifier' => 'session',
183+
],
184+
],
185+
],
186+
],
187+
]);
188+
189+
$this->assertTrue($container->hasAlias(MessageStoreInterface::class.' $main'));
190+
$this->assertTrue($container->hasAlias('.'.MessageStoreInterface::class.' $memory_main'));
191+
$this->assertTrue($container->hasAlias(MessageStoreInterface::class.' $session'));
192+
$this->assertTrue($container->hasAlias('.'.MessageStoreInterface::class.' $session_session'));
193+
}
194+
128195
public function testAgentHasTag()
129196
{
130197
$container = $this->buildContainer([
@@ -2895,6 +2962,27 @@ private function getFullConfig(): array
28952962
],
28962963
],
28972964
],
2965+
'message_store' => [
2966+
'cache' => [
2967+
'my_cache_message_store' => [
2968+
'service' => 'cache.system',
2969+
],
2970+
'my_cache_message_store_with_custom_cache_key' => [
2971+
'service' => 'cache.system',
2972+
'key' => 'foo',
2973+
],
2974+
],
2975+
'memory' => [
2976+
'my_memory_message_store' => [
2977+
'identifier' => '_memory',
2978+
],
2979+
],
2980+
'session' => [
2981+
'my_session_message_store' => [
2982+
'identifier' => 'session',
2983+
],
2984+
],
2985+
],
28982986
'vectorizer' => [
28992987
'test_vectorizer' => [
29002988
'platform' => 'mistral_platform_service_id',

src/chat/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"phpstan/phpstan": "^2.0",
2828
"phpstan/phpstan-strict-rules": "^2.0",
2929
"phpunit/phpunit": "^11.5.13",
30+
"symfony/dependency-injection": "^7.3|^8.0",
31+
"symfony/console": "^7.3|^8.0",
3032
"symfony/http-foundation": "^7.3|^8.0",
3133
"psr/cache": "^3.0"
3234
},

src/chat/doc/index.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,25 @@ This leads to a store implementing two methods::
8484
// Implementation to drop the store (and related messages)
8585
}
8686
}
87+
88+
Commands
89+
--------
90+
91+
While using the `Chat` component in your Symfony application along with the ``AiBundle``,
92+
you can use the ``bin/console ai:message-store:setup`` command to initialize the message store and ``bin/console ai:message-store:drop`` to clean up the message store:
93+
94+
.. code-block:: yaml
95+
96+
# config/packages/ai.yaml
97+
ai:
98+
# ...
99+
100+
message_store:
101+
chroma_db:
102+
symfonycon:
103+
collection: 'symfony_blog'
104+
105+
.. code-block:: terminal
106+
107+
$ php bin/console ai:message-store:setup symfonycon
108+
$ php bin/console ai:message-store:drop symfonycon

src/chat/phpstan.dist.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ parameters:
66
paths:
77
- src/
88
- tests/
9+
treatPhpDocTypesAsCertain: false
910
ignoreErrors:
1011
-
1112
message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#"

src/chat/src/Bridge/Local/InMemoryStore.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,33 @@
2020
*/
2121
final class InMemoryStore implements ManagedStoreInterface, MessageStoreInterface
2222
{
23-
private MessageBag $messages;
23+
/**
24+
* @var MessageBag[]
25+
*/
26+
private array $messages = [];
27+
28+
public function __construct(
29+
private readonly string $identifier = '_message_store_memory',
30+
) {
31+
}
2432

2533
public function setup(array $options = []): void
2634
{
27-
$this->messages = new MessageBag();
35+
$this->messages[$this->identifier] = new MessageBag();
2836
}
2937

3038
public function save(MessageBag $messages): void
3139
{
32-
$this->messages = $messages;
40+
$this->messages[$this->identifier] = $messages;
3341
}
3442

3543
public function load(): MessageBag
3644
{
37-
return $this->messages ?? new MessageBag();
45+
return $this->messages[$this->identifier] ?? new MessageBag();
3846
}
3947

4048
public function drop(): void
4149
{
42-
$this->messages = new MessageBag();
50+
$this->messages[$this->identifier] = new MessageBag();
4351
}
4452
}

0 commit comments

Comments
 (0)