Skip to content

Commit fb1cddb

Browse files
committed
feature #734 [AI Bundle][Chat] Add lifecycle commands (Guikingone)
This PR was merged into the main branch. Discussion ---------- [AI Bundle][Chat] Add lifecycle commands | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | yes | Issues | Fix none | License | MIT Hi 👋🏻 This PR aims to introduce commands to handle message store lifecycle, it also introduce the configuration for message stores. Commits ------- 0185201 feat(core): commands added for message store + configuration
2 parents ab402e0 + 0185201 commit fb1cddb

File tree

17 files changed

+770
-6
lines changed

17 files changed

+770
-6
lines changed

.github/workflows/integration-tests.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ jobs:
7575
run: ../link
7676

7777
- name: Run commands examples
78-
run: php examples/commands/stores.php
78+
run: |
79+
php examples/commands/stores.php
80+
php examples/commands/message-stores.php
7981
8082
demo:
8183
runs-on: ubuntu-latest

docs/components/chat.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+
cache:
102+
symfonycon:
103+
service: 'cache.app'
104+
105+
.. code-block:: terminal
106+
107+
$ php bin/console ai:message-store:setup symfonycon
108+
$ php bin/console ai:message-store:drop symfonycon
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
require_once dirname(__DIR__).'/bootstrap.php';
13+
14+
use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore;
15+
use Symfony\AI\Chat\Bridge\Local\CacheStore;
16+
use Symfony\AI\Chat\Bridge\Local\InMemoryStore;
17+
use Symfony\AI\Chat\Command\DropStoreCommand;
18+
use Symfony\AI\Chat\Command\SetupStoreCommand;
19+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
20+
use Symfony\Component\Console\Application;
21+
use Symfony\Component\Console\Input\ArrayInput;
22+
use Symfony\Component\Console\Output\ConsoleOutput;
23+
use Symfony\Component\DependencyInjection\ServiceLocator;
24+
use Symfony\Component\HttpFoundation\Request;
25+
use Symfony\Component\HttpFoundation\RequestStack;
26+
use Symfony\Component\HttpFoundation\Session\Session;
27+
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
28+
29+
$factories = [
30+
'cache' => static fn (): CacheStore => new CacheStore(new ArrayAdapter(), cacheKey: 'symfony'),
31+
'memory' => static fn (): InMemoryStore => new InMemoryStore('symfony'),
32+
'session' => static function (): SessionStore {
33+
$request = Request::create('/');
34+
$request->setSession(new Session(new MockArraySessionStorage()));
35+
36+
$requestStack = new RequestStack();
37+
$requestStack->push($request);
38+
39+
return new SessionStore($requestStack, 'symfony');
40+
},
41+
];
42+
43+
$storesIds = array_keys($factories);
44+
45+
$application = new Application();
46+
$application->setAutoExit(false);
47+
$application->setCatchExceptions(false);
48+
$application->addCommands([
49+
new SetupStoreCommand(new ServiceLocator($factories)),
50+
new DropStoreCommand(new ServiceLocator($factories)),
51+
]);
52+
53+
foreach ($storesIds as $store) {
54+
$setupOutputCode = $application->run(new ArrayInput([
55+
'command' => 'ai:message-store:setup',
56+
'store' => $store,
57+
]), new ConsoleOutput());
58+
59+
$dropOutputCode = $application->run(new ArrayInput([
60+
'command' => 'ai:message-store:drop',
61+
'store' => $store,
62+
'--force' => true,
63+
]), new ConsoleOutput());
64+
}

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
@@ -697,6 +697,35 @@
697697
->end()
698698
->end()
699699
->end()
700+
->arrayNode('message_store')
701+
->children()
702+
->arrayNode('memory')
703+
->useAttributeAsKey('name')
704+
->arrayPrototype()
705+
->children()
706+
->stringNode('identifier')->cannotBeEmpty()->end()
707+
->end()
708+
->end()
709+
->end()
710+
->arrayNode('cache')
711+
->useAttributeAsKey('name')
712+
->arrayPrototype()
713+
->children()
714+
->stringNode('service')->cannotBeEmpty()->defaultValue('cache.app')->end()
715+
->stringNode('key')->end()
716+
->end()
717+
->end()
718+
->end()
719+
->arrayNode('session')
720+
->useAttributeAsKey('name')
721+
->arrayPrototype()
722+
->children()
723+
->stringNode('identifier')->cannotBeEmpty()->end()
724+
->end()
725+
->end()
726+
->end()
727+
->end()
728+
->end()
700729
->arrayNode('vectorizer')
701730
->info('Vectorizers for converting strings to Vector objects and transforming TextDocument arrays to VectorDocument arrays')
702731
->useAttributeAsKey('name')

src/ai-bundle/config/services.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use Symfony\AI\AiBundle\Profiler\DataCollector;
2727
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
2828
use Symfony\AI\AiBundle\Security\EventListener\IsGrantedToolAttributeListener;
29+
use Symfony\AI\Chat\Command\DropStoreCommand as DropMessageStoreCommand;
30+
use Symfony\AI\Chat\Command\SetupStoreCommand as SetupMessageStoreCommand;
2931
use Symfony\AI\Platform\Bridge\AiMlApi\ModelCatalog as AiMlApiModelCatalog;
3032
use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract;
3133
use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog;
@@ -224,5 +226,15 @@
224226
tagged_locator('ai.platform', 'name'),
225227
])
226228
->tag('console.command')
229+
->set('ai.command.setup_message_store', SetupMessageStoreCommand::class)
230+
->args([
231+
tagged_locator('ai.message_store', 'name'),
232+
])
233+
->tag('console.command')
234+
->set('ai.command.drop_message_store', DropMessageStoreCommand::class)
235+
->args([
236+
tagged_locator('ai.message_store', 'name'),
237+
])
238+
->tag('console.command')
227239
;
228240
};

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;
@@ -163,6 +165,21 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
163165
$builder->removeDefinition('ai.command.drop_store');
164166
}
165167

168+
foreach ($config['message_store'] ?? [] as $type => $store) {
169+
$this->processMessageStoreConfig($type, $store, $builder);
170+
}
171+
172+
$messageStores = array_keys($builder->findTaggedServiceIds('ai.message_store'));
173+
174+
if (1 === \count($messageStores)) {
175+
$builder->setAlias(MessageStoreInterface::class, reset($messageStores));
176+
}
177+
178+
if ([] === $messageStores) {
179+
$builder->removeDefinition('ai.command.setup_message_store');
180+
$builder->removeDefinition('ai.command.drop_message_store');
181+
}
182+
166183
foreach ($config['vectorizer'] ?? [] as $vectorizerName => $vectorizer) {
167184
$this->processVectorizerConfig($vectorizerName, $vectorizer, $builder);
168185
}
@@ -1254,6 +1271,62 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde
12541271
}
12551272
}
12561273

1274+
/**
1275+
* @param array<string, mixed> $messageStores
1276+
*/
1277+
private function processMessageStoreConfig(string $type, array $messageStores, ContainerBuilder $container): void
1278+
{
1279+
if ('memory' === $type) {
1280+
foreach ($messageStores as $name => $messageStore) {
1281+
$definition = new Definition(InMemoryStore::class);
1282+
$definition
1283+
->setArgument(0, $messageStore['identifier'])
1284+
->addTag('ai.message_store');
1285+
1286+
$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
1287+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
1288+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
1289+
}
1290+
}
1291+
1292+
if ('cache' === $type) {
1293+
foreach ($messageStores as $name => $messageStore) {
1294+
$arguments = [
1295+
new Reference($messageStore['service']),
1296+
];
1297+
1298+
if (\array_key_exists('key', $messageStore)) {
1299+
$arguments['key'] = $messageStore['key'];
1300+
}
1301+
1302+
$definition = new Definition(CacheStore::class);
1303+
$definition
1304+
->setArguments($arguments)
1305+
->addTag('ai.message_store');
1306+
1307+
$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
1308+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
1309+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
1310+
}
1311+
}
1312+
1313+
if ('session' === $type) {
1314+
foreach ($messageStores as $name => $messageStore) {
1315+
$definition = new Definition(SessionStore::class);
1316+
$definition
1317+
->setArguments([
1318+
new Reference('request_stack'),
1319+
$messageStore['identifier'],
1320+
])
1321+
->addTag('ai.message_store');
1322+
1323+
$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
1324+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
1325+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
1326+
}
1327+
}
1328+
}
1329+
12571330
/**
12581331
* @param array<string, mixed> $config
12591332
*/

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([
@@ -2943,6 +3010,27 @@ private function getFullConfig(): array
29433010
],
29443011
],
29453012
],
3013+
'message_store' => [
3014+
'cache' => [
3015+
'my_cache_message_store' => [
3016+
'service' => 'cache.system',
3017+
],
3018+
'my_cache_message_store_with_custom_cache_key' => [
3019+
'service' => 'cache.system',
3020+
'key' => 'foo',
3021+
],
3022+
],
3023+
'memory' => [
3024+
'my_memory_message_store' => [
3025+
'identifier' => '_memory',
3026+
],
3027+
],
3028+
'session' => [
3029+
'my_session_message_store' => [
3030+
'identifier' => 'session',
3031+
],
3032+
],
3033+
],
29463034
'vectorizer' => [
29473035
'test_vectorizer' => [
29483036
'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
},

0 commit comments

Comments
 (0)