diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 0ad9e775f..41a74214e 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -499,7 +499,9 @@ ->arrayPrototype() ->children() ->stringNode('service')->cannotBeEmpty()->defaultValue('cache.app')->end() - ->stringNode('cache_key')->end() + ->stringNode('cache_key') + ->info('The name of the store will be used if the key is not set') + ->end() ->stringNode('strategy')->end() ->end() ->end() @@ -651,7 +653,7 @@ ->stringNode('collection_name')->cannotBeEmpty()->end() ->integerNode('dimensions')->end() ->stringNode('distance')->end() - ->booleanNode('async')->defaultFalse()->end() + ->booleanNode('async')->end() ->end() ->end() ->end() @@ -772,20 +774,15 @@ ->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() + ->stringNode('key') + ->info('The name of the message store will be used if the key is not set') + ->end() + ->integerNode('ttl')->end() ->end() ->end() ->end() @@ -799,6 +796,14 @@ ->end() ->end() ->end() + ->arrayNode('memory') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->stringNode('identifier')->cannotBeEmpty()->end() + ->end() + ->end() + ->end() ->arrayNode('pogocache') ->useAttributeAsKey('name') ->arrayPrototype() diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index c7decae7a..6b3c5da6a 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -37,6 +37,7 @@ use Symfony\AI\AiBundle\Profiler\TraceableToolbox; use Symfony\AI\AiBundle\Security\Attribute\IsGrantedTool; use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore; +use Symfony\AI\Chat\Bridge\Local\CacheStore as CacheMessageStore; use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore as MeilisearchMessageStore; use Symfony\AI\Chat\Bridge\Pogocache\MessageStore as PogocacheMessageStore; use Symfony\AI\Chat\Bridge\Redis\MessageStore as RedisMessageStore; @@ -917,10 +918,6 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde new Definition(DistanceCalculator::class), ]; - if (\array_key_exists('cache_key', $store) && null !== $store['cache_key']) { - $arguments[2] = $store['cache_key']; - } - if (\array_key_exists('strategy', $store) && null !== $store['strategy']) { if (!$container->hasDefinition('ai.store.distance_calculator.'.$name)) { $distanceCalculatorDefinition = new Definition(DistanceCalculator::class); @@ -932,10 +929,14 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $arguments[1] = new Reference('ai.store.distance_calculator.'.$name); } + $arguments[2] = \array_key_exists('cache_key', $store) && null !== $store['cache_key'] + ? $store['cache_key'] + : $name; + $definition = new Definition(CacheStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setArguments($arguments) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1470,32 +1471,22 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde */ 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']), + $messageStore['key'] ?? $name, ]; - if (\array_key_exists('key', $messageStore)) { - $arguments['key'] = $messageStore['key']; + if (\array_key_exists('ttl', $messageStore)) { + $arguments[2] = $messageStore['ttl']; } - $definition = new Definition(CacheStore::class); + $definition = new Definition(CacheMessageStore::class); $definition + ->setLazy(true) ->setArguments($arguments) + ->addTag('proxy', ['interface' => MessageStoreInterface::class]) ->addTag('ai.message_store'); $container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition); @@ -1508,12 +1499,29 @@ private function processMessageStoreConfig(string $type, array $messageStores, C foreach ($messageStores as $name => $messageStore) { $definition = new Definition(MeilisearchMessageStore::class); $definition + ->setLazy(true) ->setArguments([ $messageStore['endpoint'], $messageStore['api_key'], new Reference(ClockInterface::class), $messageStore['index_name'], ]) + ->addTag('proxy', ['interface' => MessageStoreInterface::class]) + ->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 ('memory' === $type) { + foreach ($messageStores as $name => $messageStore) { + $definition = new Definition(InMemoryStore::class); + $definition + ->setLazy(true) + ->setArgument(0, $messageStore['identifier']) + ->addTag('proxy', ['interface' => MessageStoreInterface::class]) ->addTag('ai.message_store'); $container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition); @@ -1526,12 +1534,14 @@ private function processMessageStoreConfig(string $type, array $messageStores, C foreach ($messageStores as $name => $messageStore) { $definition = new Definition(PogocacheMessageStore::class); $definition + ->setLazy(true) ->setArguments([ new Reference('http_client'), $messageStore['endpoint'], $messageStore['password'], $messageStore['key'], ]) + ->addTag('proxy', ['interface' => MessageStoreInterface::class]) ->addTag('ai.message_store'); $container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition); @@ -1551,11 +1561,13 @@ private function processMessageStoreConfig(string $type, array $messageStores, C $definition = new Definition(RedisMessageStore::class); $definition + ->setLazy(true) ->setArguments([ $redisClient, $messageStore['index_name'], new Reference('serializer'), ]) + ->addTag('proxy', ['interface' => MessageStoreInterface::class]) ->addTag('ai.message_store'); $container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition); @@ -1568,10 +1580,12 @@ private function processMessageStoreConfig(string $type, array $messageStores, C foreach ($messageStores as $name => $messageStore) { $definition = new Definition(SessionStore::class); $definition + ->setLazy(true) ->setArguments([ new Reference('request_stack'), $messageStore['identifier'], ]) + ->addTag('proxy', ['interface' => MessageStoreInterface::class]) ->addTag('ai.message_store'); $container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition); diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index dfcfbda76..2975d8d84 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -28,6 +28,7 @@ use Symfony\AI\Store\Document\Transformer\TextTrimTransformer; use Symfony\AI\Store\Document\Vectorizer; use Symfony\AI\Store\StoreInterface; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -423,11 +424,12 @@ public function testCacheStoreWithCustomStrategyCanBeConfigured() $definition = $container->getDefinition('ai.store.cache.my_cache_store_with_custom_strategy'); - $this->assertCount(2, $definition->getArguments()); + $this->assertCount(3, $definition->getArguments()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); $this->assertSame('cache.system', (string) $definition->getArgument(0)); $this->assertInstanceOf(Reference::class, $definition->getArgument(1)); $this->assertSame('ai.store.distance_calculator.my_cache_store_with_custom_strategy', (string) $definition->getArgument(1)); + $this->assertSame('my_cache_store_with_custom_strategy', $definition->getArgument(2)); } public function testCacheStoreWithCustomStrategyAndKeyCanBeConfigured() @@ -454,9 +456,9 @@ public function testCacheStoreWithCustomStrategyAndKeyCanBeConfigured() $this->assertCount(3, $definition->getArguments()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); $this->assertSame('cache.system', (string) $definition->getArgument(0)); - $this->assertSame('random', $definition->getArgument(2)); $this->assertInstanceOf(Reference::class, $definition->getArgument(1)); $this->assertSame('ai.store.distance_calculator.my_cache_store_with_custom_strategy', (string) $definition->getArgument(1)); + $this->assertSame('random', $definition->getArgument(2)); } public function testInMemoryStoreWithoutCustomStrategyCanBeConfigured() @@ -2793,6 +2795,234 @@ public function testVectorizerModelBooleanOptionsArePreserved() $this->assertSame('text-embedding-3-small?normalize=false&cache=true&nested%5Bbool%5D=false', $vectorizerDefinition->getArgument(1)); } + public function testCacheMessageStoreCanBeConfiguredWithCustomKey() + { + $container = $this->buildContainer([ + 'ai' => [ + 'message_store' => [ + 'cache' => [ + 'custom' => [ + 'service' => 'cache.app', + 'key' => 'custom', + ], + ], + ], + ], + ]); + + $cacheMessageStoreDefinition = $container->getDefinition('ai.message_store.cache.custom'); + + $this->assertInstanceOf(Reference::class, $cacheMessageStoreDefinition->getArgument(0)); + $this->assertSame('cache.app', (string) $cacheMessageStoreDefinition->getArgument(0)); + + $this->assertSame('custom', (string) $cacheMessageStoreDefinition->getArgument(1)); + + $this->assertTrue($cacheMessageStoreDefinition->hasTag('proxy')); + $this->assertSame([['interface' => MessageStoreInterface::class]], $cacheMessageStoreDefinition->getTag('proxy')); + $this->assertTrue($cacheMessageStoreDefinition->hasTag('ai.message_store')); + } + + public function testCacheMessageStoreCanBeConfiguredWithCustomTtl() + { + $container = $this->buildContainer([ + 'ai' => [ + 'message_store' => [ + 'cache' => [ + 'custom' => [ + 'service' => 'cache.app', + 'ttl' => 3600, + ], + ], + ], + ], + ]); + + $cacheMessageStoreDefinition = $container->getDefinition('ai.message_store.cache.custom'); + + $this->assertTrue($cacheMessageStoreDefinition->isLazy()); + $this->assertInstanceOf(Reference::class, $cacheMessageStoreDefinition->getArgument(0)); + $this->assertSame('cache.app', (string) $cacheMessageStoreDefinition->getArgument(0)); + + $this->assertSame('custom', (string) $cacheMessageStoreDefinition->getArgument(1)); + $this->assertSame(3600, (int) $cacheMessageStoreDefinition->getArgument(2)); + + $this->assertTrue($cacheMessageStoreDefinition->hasTag('proxy')); + $this->assertSame([['interface' => MessageStoreInterface::class]], $cacheMessageStoreDefinition->getTag('proxy')); + $this->assertTrue($cacheMessageStoreDefinition->hasTag('ai.message_store')); + } + + public function testMeilisearchMessageStoreIsConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'message_store' => [ + 'meilisearch' => [ + 'custom' => [ + 'endpoint' => 'http://127.0.0.1:7700', + 'api_key' => 'foo', + 'index_name' => 'test', + ], + ], + ], + ], + ]); + + $meilisearchMessageStoreDefinition = $container->getDefinition('ai.message_store.meilisearch.custom'); + + $this->assertTrue($meilisearchMessageStoreDefinition->isLazy()); + $this->assertSame('http://127.0.0.1:7700', $meilisearchMessageStoreDefinition->getArgument(0)); + $this->assertSame('foo', $meilisearchMessageStoreDefinition->getArgument(1)); + $this->assertInstanceOf(Reference::class, $meilisearchMessageStoreDefinition->getArgument(2)); + $this->assertSame(ClockInterface::class, (string) $meilisearchMessageStoreDefinition->getArgument(2)); + $this->assertSame('test', $meilisearchMessageStoreDefinition->getArgument(3)); + + $this->assertTrue($meilisearchMessageStoreDefinition->hasTag('proxy')); + $this->assertSame([['interface' => MessageStoreInterface::class]], $meilisearchMessageStoreDefinition->getTag('proxy')); + $this->assertTrue($meilisearchMessageStoreDefinition->hasTag('ai.message_store')); + } + + public function testMemoryMessageStoreCanBeConfiguredWithCustomKey() + { + $container = $this->buildContainer([ + 'ai' => [ + 'message_store' => [ + 'memory' => [ + 'custom' => [ + 'identifier' => 'foo', + ], + ], + ], + ], + ]); + + $memoryMessageStoreDefinition = $container->getDefinition('ai.message_store.memory.custom'); + + $this->assertTrue($memoryMessageStoreDefinition->isLazy()); + $this->assertSame('foo', $memoryMessageStoreDefinition->getArgument(0)); + + $this->assertTrue($memoryMessageStoreDefinition->hasTag('proxy')); + $this->assertSame([['interface' => MessageStoreInterface::class]], $memoryMessageStoreDefinition->getTag('proxy')); + $this->assertTrue($memoryMessageStoreDefinition->hasTag('ai.message_store')); + } + + public function testPogocacheMessageStoreIsConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'message_store' => [ + 'pogocache' => [ + 'custom' => [ + 'endpoint' => 'http://127.0.0.1:9401', + 'password' => 'foo', + 'key' => 'bar', + ], + ], + ], + ], + ]); + + $pogocacheMessageStoreDefinition = $container->getDefinition('ai.message_store.pogocache.custom'); + + $this->assertTrue($pogocacheMessageStoreDefinition->isLazy()); + $this->assertInstanceOf(Reference::class, $pogocacheMessageStoreDefinition->getArgument(0)); + $this->assertSame('http_client', (string) $pogocacheMessageStoreDefinition->getArgument(0)); + $this->assertSame('http://127.0.0.1:9401', $pogocacheMessageStoreDefinition->getArgument(1)); + $this->assertSame('foo', $pogocacheMessageStoreDefinition->getArgument(2)); + $this->assertSame('bar', $pogocacheMessageStoreDefinition->getArgument(3)); + + $this->assertTrue($pogocacheMessageStoreDefinition->hasTag('proxy')); + $this->assertSame([['interface' => MessageStoreInterface::class]], $pogocacheMessageStoreDefinition->getTag('proxy')); + $this->assertTrue($pogocacheMessageStoreDefinition->hasTag('ai.message_store')); + } + + public function testRedisMessageStoreIsConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'message_store' => [ + 'redis' => [ + 'custom' => [ + 'endpoint' => 'http://127.0.0.1:9401', + 'index_name' => 'foo', + 'connection_parameters' => [ + 'foo' => 'bar', + ], + ], + ], + ], + ], + ]); + + $redisMessageStoreDefinition = $container->getDefinition('ai.message_store.redis.custom'); + + $this->assertTrue($redisMessageStoreDefinition->isLazy()); + $this->assertInstanceOf(Definition::class, $redisMessageStoreDefinition->getArgument(0)); + $this->assertSame(\Redis::class, $redisMessageStoreDefinition->getArgument(0)->getClass()); + $this->assertSame('foo', $redisMessageStoreDefinition->getArgument(1)); + $this->assertInstanceOf(Reference::class, $redisMessageStoreDefinition->getArgument(2)); + $this->assertSame('serializer', (string) $redisMessageStoreDefinition->getArgument(2)); + + $this->assertTrue($redisMessageStoreDefinition->hasTag('proxy')); + $this->assertSame([['interface' => MessageStoreInterface::class]], $redisMessageStoreDefinition->getTag('proxy')); + $this->assertTrue($redisMessageStoreDefinition->hasTag('ai.message_store')); + } + + public function testRedisMessageStoreIsConfiguredWithCustomClient() + { + $container = $this->buildContainer([ + 'ai' => [ + 'message_store' => [ + 'redis' => [ + 'custom' => [ + 'endpoint' => 'http://127.0.0.1:9401', + 'index_name' => 'foo', + 'client' => 'custom.redis', + ], + ], + ], + ], + ]); + + $redisMessageStoreDefinition = $container->getDefinition('ai.message_store.redis.custom'); + + $this->assertTrue($redisMessageStoreDefinition->isLazy()); + $this->assertInstanceOf(Reference::class, $redisMessageStoreDefinition->getArgument(0)); + $this->assertSame('custom.redis', (string) $redisMessageStoreDefinition->getArgument(0)); + $this->assertSame('foo', $redisMessageStoreDefinition->getArgument(1)); + $this->assertInstanceOf(Reference::class, $redisMessageStoreDefinition->getArgument(2)); + $this->assertSame('serializer', (string) $redisMessageStoreDefinition->getArgument(2)); + + $this->assertTrue($redisMessageStoreDefinition->hasTag('proxy')); + $this->assertSame([['interface' => MessageStoreInterface::class]], $redisMessageStoreDefinition->getTag('proxy')); + $this->assertTrue($redisMessageStoreDefinition->hasTag('ai.message_store')); + } + + public function testSessionMessageStoreIsConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'message_store' => [ + 'session' => [ + 'custom' => [ + 'identifier' => 'foo', + ], + ], + ], + ], + ]); + + $sessionMessageStoreDefinition = $container->getDefinition('ai.message_store.session.custom'); + + $this->assertTrue($sessionMessageStoreDefinition->isLazy()); + $this->assertInstanceOf(Reference::class, $sessionMessageStoreDefinition->getArgument(0)); + $this->assertSame('request_stack', (string) $sessionMessageStoreDefinition->getArgument(0)); + $this->assertSame('foo', (string) $sessionMessageStoreDefinition->getArgument(1)); + + $this->assertTrue($sessionMessageStoreDefinition->hasTag('proxy')); + $this->assertSame([['interface' => MessageStoreInterface::class]], $sessionMessageStoreDefinition->getTag('proxy')); + $this->assertTrue($sessionMessageStoreDefinition->hasTag('ai.message_store')); + } + private function buildContainer(array $configuration): ContainerBuilder { $container = new ContainerBuilder(); @@ -3032,6 +3262,24 @@ private function getFullConfig(): array 'distance' => 'Cosine', 'async' => false, ], + 'my_custom_dimensions_qdrant_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'api_key' => 'test', + 'collection_name' => 'foo', + 'dimensions' => 768, + ], + 'my_custom_distance_qdrant_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'api_key' => 'test', + 'collection_name' => 'foo', + 'distance' => 'Cosine', + ], + 'my_async_qdrant_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'api_key' => 'test', + 'collection_name' => 'foo', + 'async' => false, + ], ], 'redis' => [ 'my_redis_store' => [ diff --git a/src/chat/src/Bridge/Local/CacheStore.php b/src/chat/src/Bridge/Local/CacheStore.php index d54ed455b..0b2626fa3 100644 --- a/src/chat/src/Bridge/Local/CacheStore.php +++ b/src/chat/src/Bridge/Local/CacheStore.php @@ -24,7 +24,7 @@ final class CacheStore implements ManagedStoreInterface, MessageStoreInterface { public function __construct( private readonly CacheItemPoolInterface $cache, - private readonly string $cacheKey, + private readonly string $cacheKey = '_message_store_cache', private readonly int $ttl = 86400, ) { if (!interface_exists(CacheItemPoolInterface::class)) {