Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/integration-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ jobs:
run: ../link

- name: Run commands examples
run: php examples/commands/stores.php
run: |
php examples/commands/stores.php
php examples/commands/message-stores.php

demo:
runs-on: ubuntu-latest
Expand Down
22 changes: 22 additions & 0 deletions docs/components/chat.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,25 @@ This leads to a store implementing two methods::
// Implementation to drop the store (and related messages)
}
}

Commands
--------

While using the `Chat` component in your Symfony application along with the ``AiBundle``,
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:

.. code-block:: yaml

# config/packages/ai.yaml
ai:
# ...

message_store:
cache:
symfonycon:
service: 'cache.app'

.. code-block:: terminal

$ php bin/console ai:message-store:setup symfonycon
$ php bin/console ai:message-store:drop symfonycon
64 changes: 64 additions & 0 deletions examples/commands/message-stores.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

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

require_once dirname(__DIR__).'/bootstrap.php';

use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore;
use Symfony\AI\Chat\Bridge\Local\CacheStore;
use Symfony\AI\Chat\Bridge\Local\InMemoryStore;
use Symfony\AI\Chat\Command\DropStoreCommand;
use Symfony\AI\Chat\Command\SetupStoreCommand;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;

$factories = [
'cache' => static fn (): CacheStore => new CacheStore(new ArrayAdapter(), cacheKey: 'symfony'),
'memory' => static fn (): InMemoryStore => new InMemoryStore('symfony'),
'session' => static function (): SessionStore {
$request = Request::create('/');
$request->setSession(new Session(new MockArraySessionStorage()));

$requestStack = new RequestStack();
$requestStack->push($request);

return new SessionStore($requestStack, 'symfony');
},
];

$storesIds = array_keys($factories);

$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions(false);
$application->addCommands([
new SetupStoreCommand(new ServiceLocator($factories)),
new DropStoreCommand(new ServiceLocator($factories)),
]);

foreach ($storesIds as $store) {
$setupOutputCode = $application->run(new ArrayInput([
'command' => 'ai:message-store:setup',
'store' => $store,
]), new ConsoleOutput());

$dropOutputCode = $application->run(new ArrayInput([
'command' => 'ai:message-store:drop',
'store' => $store,
'--force' => true,
]), new ConsoleOutput());
}
1 change: 1 addition & 0 deletions src/ai-bundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"require": {
"php": ">=8.2",
"symfony/ai-agent": "@dev",
"symfony/ai-chat": "@dev",
"symfony/ai-platform": "@dev",
"symfony/ai-store": "@dev",
"symfony/config": "^7.3|^8.0",
Expand Down
29 changes: 29 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,35 @@
->end()
->end()
->end()
->arrayNode('message_store')
->children()
->arrayNode('memory')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->stringNode('identifier')->cannotBeEmpty()->end()
->end()
->end()
->end()
->arrayNode('cache')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->stringNode('service')->cannotBeEmpty()->defaultValue('cache.app')->end()
->stringNode('key')->end()
->end()
->end()
->end()
->arrayNode('session')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->stringNode('identifier')->cannotBeEmpty()->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('vectorizer')
->info('Vectorizers for converting strings to Vector objects and transforming TextDocument arrays to VectorDocument arrays')
->useAttributeAsKey('name')
Expand Down
12 changes: 12 additions & 0 deletions src/ai-bundle/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use Symfony\AI\AiBundle\Profiler\DataCollector;
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
use Symfony\AI\AiBundle\Security\EventListener\IsGrantedToolAttributeListener;
use Symfony\AI\Chat\Command\DropStoreCommand as DropMessageStoreCommand;
use Symfony\AI\Chat\Command\SetupStoreCommand as SetupMessageStoreCommand;
use Symfony\AI\Platform\Bridge\AiMlApi\ModelCatalog as AiMlApiModelCatalog;
use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract;
use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog;
Expand Down Expand Up @@ -224,5 +226,15 @@
tagged_locator('ai.platform', 'name'),
])
->tag('console.command')
->set('ai.command.setup_message_store', SetupMessageStoreCommand::class)
->args([
tagged_locator('ai.message_store', 'name'),
])
->tag('console.command')
->set('ai.command.drop_message_store', DropMessageStoreCommand::class)
->args([
tagged_locator('ai.message_store', 'name'),
])
->tag('console.command')
;
};
73 changes: 73 additions & 0 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
use Symfony\AI\AiBundle\Profiler\TraceablePlatform;
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
use Symfony\AI\AiBundle\Security\Attribute\IsGrantedTool;
use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory;
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory as CerebrasPlatformFactory;
Expand Down Expand Up @@ -163,6 +165,21 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
$builder->removeDefinition('ai.command.drop_store');
}

foreach ($config['message_store'] ?? [] as $type => $store) {
$this->processMessageStoreConfig($type, $store, $builder);
}

$messageStores = array_keys($builder->findTaggedServiceIds('ai.message_store'));

if (1 === \count($messageStores)) {
$builder->setAlias(MessageStoreInterface::class, reset($messageStores));
}

if ([] === $messageStores) {
$builder->removeDefinition('ai.command.setup_message_store');
$builder->removeDefinition('ai.command.drop_message_store');
}

foreach ($config['vectorizer'] ?? [] as $vectorizerName => $vectorizer) {
$this->processVectorizerConfig($vectorizerName, $vectorizer, $builder);
}
Expand Down Expand Up @@ -1254,6 +1271,62 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde
}
}

/**
* @param array<string, mixed> $messageStores
*/
private function processMessageStoreConfig(string $type, array $messageStores, ContainerBuilder $container): void
{
if ('memory' === $type) {
foreach ($messageStores as $name => $messageStore) {
$definition = new Definition(InMemoryStore::class);
$definition
->setArgument(0, $messageStore['identifier'])
->addTag('ai.message_store');

$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
}
}

if ('cache' === $type) {
foreach ($messageStores as $name => $messageStore) {
$arguments = [
new Reference($messageStore['service']),
];

if (\array_key_exists('key', $messageStore)) {
$arguments['key'] = $messageStore['key'];
}

$definition = new Definition(CacheStore::class);
$definition
->setArguments($arguments)
->addTag('ai.message_store');

$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
}
}

if ('session' === $type) {
foreach ($messageStores as $name => $messageStore) {
$definition = new Definition(SessionStore::class);
$definition
->setArguments([
new Reference('request_stack'),
$messageStore['identifier'],
])
->addTag('ai.message_store');

$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
}
}
}

/**
* @param array<string, mixed> $config
*/
Expand Down
88 changes: 88 additions & 0 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Symfony\AI\Agent\MultiAgent\Handoff;
use Symfony\AI\Agent\MultiAgent\MultiAgent;
use Symfony\AI\AiBundle\AiBundle;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Store\Document\Filter\TextContainsFilter;
use Symfony\AI\Store\Document\Loader\InMemoryLoader;
use Symfony\AI\Store\Document\Transformer\TextTrimTransformer;
Expand Down Expand Up @@ -58,6 +59,30 @@ public function testStoreCommandsArentDefinedWithoutStore()
$this->assertSame([
'ai.command.setup_store' => true,
'ai.command.drop_store' => true,
'ai.command.setup_message_store' => true,
'ai.command.drop_message_store' => true,
], $container->getRemovedIds());
}

public function testMessageStoreCommandsArentDefinedWithoutMessageStore()
{
$container = $this->buildContainer([
'ai' => [
'agent' => [
'my_agent' => [
'model' => 'gpt-4',
],
],
],
]);

$this->assertFalse($container->hasDefinition('ai.command.setup_message_store'));
$this->assertFalse($container->hasDefinition('ai.command.drop_message_store'));
$this->assertSame([
'ai.command.setup_store' => true,
'ai.command.drop_store' => true,
'ai.command.setup_message_store' => true,
'ai.command.drop_message_store' => true,
], $container->getRemovedIds());
}

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

public function testMessageStoreCommandsAreDefined()
{
$container = $this->buildContainer($this->getFullConfig());

$this->assertTrue($container->hasDefinition('ai.command.setup_message_store'));

$setupStoreCommandDefinition = $container->getDefinition('ai.command.setup_message_store');
$this->assertCount(1, $setupStoreCommandDefinition->getArguments());
$this->assertArrayHasKey('console.command', $setupStoreCommandDefinition->getTags());

$this->assertTrue($container->hasDefinition('ai.command.drop_message_store'));

$dropStoreCommandDefinition = $container->getDefinition('ai.command.drop_message_store');
$this->assertCount(1, $dropStoreCommandDefinition->getArguments());
$this->assertArrayHasKey('console.command', $dropStoreCommandDefinition->getTags());
}

public function testInjectionAgentAliasIsRegistered()
{
$container = $this->buildContainer([
Expand Down Expand Up @@ -125,6 +167,31 @@ public function testInjectionStoreAliasIsRegistered()
$this->assertTrue($container->hasAlias(StoreInterface::class.' $weaviateMain'));
}

public function testInjectionMessageStoreAliasIsRegistered()
{
$container = $this->buildContainer([
'ai' => [
'message_store' => [
'memory' => [
'main' => [
'identifier' => '_memory',
],
],
'session' => [
'session' => [
'identifier' => 'session',
],
],
],
],
]);

$this->assertTrue($container->hasAlias(MessageStoreInterface::class.' $main'));
$this->assertTrue($container->hasAlias('.'.MessageStoreInterface::class.' $memory_main'));
$this->assertTrue($container->hasAlias(MessageStoreInterface::class.' $session'));
$this->assertTrue($container->hasAlias('.'.MessageStoreInterface::class.' $session_session'));
}

public function testAgentHasTag()
{
$container = $this->buildContainer([
Expand Down Expand Up @@ -2943,6 +3010,27 @@ private function getFullConfig(): array
],
],
],
'message_store' => [
'cache' => [
'my_cache_message_store' => [
'service' => 'cache.system',
],
'my_cache_message_store_with_custom_cache_key' => [
'service' => 'cache.system',
'key' => 'foo',
],
],
'memory' => [
'my_memory_message_store' => [
'identifier' => '_memory',
],
],
'session' => [
'my_session_message_store' => [
'identifier' => 'session',
],
],
],
'vectorizer' => [
'test_vectorizer' => [
'platform' => 'mistral_platform_service_id',
Expand Down
2 changes: 2 additions & 0 deletions src/chat/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^11.5.13",
"symfony/dependency-injection": "^7.3|^8.0",
"symfony/console": "^7.3|^8.0",
"symfony/http-foundation": "^7.3|^8.0",
"psr/cache": "^3.0"
},
Expand Down
Loading