From d0ee93c35d5f1035d02ad19427c9c2827e611bb5 Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Mon, 10 Nov 2025 19:08:05 +0100 Subject: [PATCH] refactor(aibundle): improvements on vector stores --- src/ai-bundle/composer.json | 2 + src/ai-bundle/config/options.php | 169 +- src/ai-bundle/src/AiBundle.php | 444 ++-- .../DependencyInjection/AiBundleTest.php | 2107 +++++++++++++++-- 4 files changed, 2239 insertions(+), 483 deletions(-) diff --git a/src/ai-bundle/composer.json b/src/ai-bundle/composer.json index 2ce1f3397..313a847fa 100644 --- a/src/ai-bundle/composer.json +++ b/src/ai-bundle/composer.json @@ -27,11 +27,13 @@ "symfony/string": "^7.3|^8.0" }, "require-dev": { + "codewithkyrian/chromadb-php": "^0.2.1 || ^0.3 || ^0.4", "google/auth": "^1.47", "mongodb/mongodb": "^1.21 || ^2.0", "phpstan/phpstan": "^2.1", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^11.5", + "probots-io/pinecone-php": "^1.0", "symfony/expression-language": "^7.3|^8.0", "symfony/security-core": "^7.3|^8.0", "symfony/translation": "^7.3|^8.0" diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 9e09b5226..be760c478 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -442,7 +442,7 @@ ->treatNullLike(['enabled' => true]) ->beforeNormalization() ->ifArray() - ->then(function (array $v) { + ->then(function (array $v): array { return [ 'enabled' => $v['enabled'] ?? true, 'services' => $v['services'] ?? $v, @@ -564,7 +564,7 @@ ->stringNode('table')->isRequired()->cannotBeEmpty()->end() ->end() ->validate() - ->ifTrue(static fn ($v) => !isset($v['dsn']) && !isset($v['http_client'])) + ->ifTrue(static fn ($v): bool => !isset($v['dsn']) && !isset($v['http_client'])) ->thenInvalid('Either "dsn" or "http_client" must be configured.') ->end() ->end() @@ -576,8 +576,11 @@ ->stringNode('account_id')->cannotBeEmpty()->end() ->stringNode('api_key')->cannotBeEmpty()->end() ->stringNode('index_name')->cannotBeEmpty()->end() - ->integerNode('dimensions')->end() - ->stringNode('metric')->end() + ->integerNode('dimensions')->isRequired()->end() + ->stringNode('metric') + ->cannotBeEmpty() + ->defaultValue('cosine') + ->end() ->stringNode('endpoint_url')->end() ->end() ->end() @@ -588,14 +591,30 @@ ->children() ->stringNode('endpoint')->cannotBeEmpty()->end() ->stringNode('table')->cannotBeEmpty()->end() - ->stringNode('field')->end() - ->stringNode('type')->end() - ->stringNode('similarity')->end() - ->integerNode('dimensions')->end() + ->stringNode('field')->cannotBeEmpty()->end() + ->stringNode('type')->cannotBeEmpty()->end() + ->stringNode('similarity')->cannotBeEmpty()->end() + ->integerNode('dimensions')->isRequired()->end() ->stringNode('quantization')->end() ->end() ->end() ->end() + ->arrayNode('mariadb') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->stringNode('connection')->cannotBeEmpty()->end() + ->stringNode('table_name')->cannotBeEmpty()->end() + ->stringNode('index_name')->cannotBeEmpty()->end() + ->stringNode('vector_field_name')->cannotBeEmpty()->end() + ->arrayNode('setup_options') + ->children() + ->integerNode('dimensions')->end() + ->end() + ->end() + ->end() + ->end() + ->end() ->arrayNode('meilisearch') ->useAttributeAsKey('name') ->arrayPrototype() @@ -603,9 +622,9 @@ ->stringNode('endpoint')->cannotBeEmpty()->end() ->stringNode('api_key')->cannotBeEmpty()->end() ->stringNode('index_name')->cannotBeEmpty()->end() - ->stringNode('embedder')->end() - ->stringNode('vector_field')->end() - ->integerNode('dimensions')->end() + ->stringNode('embedder')->cannotBeEmpty()->end() + ->stringNode('vector_field')->cannotBeEmpty()->end() + ->integerNode('dimensions')->isRequired()->end() ->floatNode('semantic_ratio') ->info('The ratio between semantic (vector) and full-text search (0.0 to 1.0). Default: 1.0 (100% semantic)') ->defaultValue(1.0) @@ -623,22 +642,6 @@ ->end() ->end() ->end() - ->arrayNode('mariadb') - ->useAttributeAsKey('name') - ->arrayPrototype() - ->children() - ->stringNode('connection')->cannotBeEmpty()->end() - ->stringNode('table_name')->cannotBeEmpty()->end() - ->stringNode('index_name')->cannotBeEmpty()->end() - ->stringNode('vector_field_name')->cannotBeEmpty()->end() - ->arrayNode('setup_options') - ->children() - ->integerNode('dimensions')->end() - ->end() - ->end() - ->end() - ->end() - ->end() ->arrayNode('milvus') ->useAttributeAsKey('name') ->arrayPrototype() @@ -647,8 +650,8 @@ ->stringNode('api_key')->isRequired()->end() ->stringNode('database')->isRequired()->end() ->stringNode('collection')->isRequired()->end() - ->stringNode('vector_field')->end() - ->integerNode('dimensions')->end() + ->stringNode('vector_field')->isRequired()->end() + ->integerNode('dimensions')->isRequired()->end() ->stringNode('metric_type')->end() ->end() ->end() @@ -664,7 +667,7 @@ ->stringNode('database')->isRequired()->end() ->stringNode('collection')->isRequired()->end() ->stringNode('index_name')->isRequired()->end() - ->stringNode('vector_field')->end() + ->stringNode('vector_field')->isRequired()->end() ->booleanNode('bulk_write')->end() ->end() ->end() @@ -679,9 +682,9 @@ ->stringNode('database')->cannotBeEmpty()->end() ->stringNode('vector_index_name')->cannotBeEmpty()->end() ->stringNode('node_name')->cannotBeEmpty()->end() - ->stringNode('vector_field')->end() - ->integerNode('dimensions')->end() - ->stringNode('distance')->end() + ->stringNode('vector_field')->isRequired()->end() + ->integerNode('dimensions')->isRequired()->end() + ->stringNode('distance')->isRequired()->end() ->booleanNode('quantization')->end() ->end() ->end() @@ -696,12 +699,40 @@ ->end() ->stringNode('namespace')->end() ->arrayNode('filter') - ->scalarPrototype()->end() + ->scalarPrototype() + ->defaultValue([]) + ->end() ->end() ->integerNode('top_k')->end() ->end() ->end() ->end() + ->arrayNode('postgres') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->stringNode('dsn')->cannotBeEmpty()->end() + ->stringNode('username')->end() + ->stringNode('password')->end() + ->stringNode('table_name')->isRequired()->end() + ->stringNode('vector_field')->isRequired()->end() + ->enumNode('distance') + ->info('Distance metric to use for vector similarity search') + ->enumFqcn(PostgresDistance::class) + ->defaultValue(PostgresDistance::L2) + ->end() + ->stringNode('dbal_connection')->cannotBeEmpty()->end() + ->end() + ->validate() + ->ifTrue(static fn (array $v): bool => !isset($v['dsn']) && !isset($v['dbal_connection'])) + ->thenInvalid('Either "dsn" or "dbal_connection" must be configured.') + ->end() + ->validate() + ->ifTrue(static fn (array $v): bool => isset($v['dsn'], $v['dbal_connection'])) + ->thenInvalid('Either "dsn" or "dbal_connection" can be configured, but not both.') + ->end() + ->end() + ->end() ->arrayNode('qdrant') ->useAttributeAsKey('name') ->arrayPrototype() @@ -709,8 +740,8 @@ ->stringNode('endpoint')->cannotBeEmpty()->end() ->stringNode('api_key')->cannotBeEmpty()->end() ->stringNode('collection_name')->cannotBeEmpty()->end() - ->integerNode('dimensions')->end() - ->stringNode('distance')->end() + ->integerNode('dimensions')->isRequired()->end() + ->stringNode('distance')->isRequired()->end() ->booleanNode('async')->end() ->end() ->end() @@ -736,32 +767,15 @@ ->end() ->end() ->validate() - ->ifTrue(static fn ($v) => !isset($v['connection_parameters']) && !isset($v['client'])) + ->ifTrue(static fn (array $v): bool => !isset($v['connection_parameters']) && !isset($v['client'])) ->thenInvalid('Either "connection_parameters" or "client" must be configured.') ->end() ->validate() - ->ifTrue(static fn ($v) => isset($v['connection_parameters']) && isset($v['client'])) + ->ifTrue(static fn (array $v): bool => isset($v['connection_parameters']) && isset($v['client'])) ->thenInvalid('Either "connection_parameters" or "client" can be configured, but not both.') ->end() ->end() ->end() - ->arrayNode('surreal_db') - ->useAttributeAsKey('name') - ->arrayPrototype() - ->children() - ->stringNode('endpoint')->cannotBeEmpty()->end() - ->stringNode('username')->cannotBeEmpty()->end() - ->stringNode('password')->cannotBeEmpty()->end() - ->stringNode('namespace')->cannotBeEmpty()->end() - ->stringNode('database')->cannotBeEmpty()->end() - ->stringNode('table')->end() - ->stringNode('vector_field')->end() - ->stringNode('strategy')->end() - ->integerNode('dimensions')->end() - ->booleanNode('namespaced_user')->end() - ->end() - ->end() - ->end() ->arrayNode('supabase') ->useAttributeAsKey('name') ->arrayPrototype() @@ -780,51 +794,42 @@ ->end() ->end() ->end() - ->arrayNode('typesense') + ->arrayNode('surrealdb') ->useAttributeAsKey('name') ->arrayPrototype() ->children() ->stringNode('endpoint')->cannotBeEmpty()->end() - ->stringNode('api_key')->isRequired()->end() - ->stringNode('collection')->isRequired()->end() - ->stringNode('vector_field')->end() - ->integerNode('dimensions')->end() + ->stringNode('username')->cannotBeEmpty()->end() + ->stringNode('password')->cannotBeEmpty()->end() + ->stringNode('namespace')->cannotBeEmpty()->end() + ->stringNode('database')->cannotBeEmpty()->end() + ->stringNode('table')->isRequired()->end() + ->stringNode('vector_field')->isRequired()->end() + ->stringNode('strategy')->isRequired()->end() + ->integerNode('dimensions')->isRequired()->end() + ->booleanNode('namespaced_user')->end() ->end() ->end() ->end() - ->arrayNode('weaviate') + ->arrayNode('typesense') ->useAttributeAsKey('name') ->arrayPrototype() ->children() ->stringNode('endpoint')->cannotBeEmpty()->end() ->stringNode('api_key')->isRequired()->end() ->stringNode('collection')->isRequired()->end() + ->stringNode('vector_field')->isRequired()->end() + ->integerNode('dimensions')->isRequired()->end() ->end() ->end() ->end() - ->arrayNode('postgres') + ->arrayNode('weaviate') ->useAttributeAsKey('name') ->arrayPrototype() ->children() - ->stringNode('dsn')->cannotBeEmpty()->end() - ->stringNode('username')->end() - ->stringNode('password')->end() - ->stringNode('table_name')->isRequired()->end() - ->stringNode('vector_field')->end() - ->enumNode('distance') - ->info('Distance metric to use for vector similarity search') - ->enumFqcn(PostgresDistance::class) - ->defaultValue(PostgresDistance::L2) - ->end() - ->stringNode('dbal_connection')->cannotBeEmpty()->end() - ->end() - ->validate() - ->ifTrue(static fn ($v) => !isset($v['dsn']) && !isset($v['dbal_connection'])) - ->thenInvalid('Either "dsn" or "dbal_connection" must be configured.') - ->end() - ->validate() - ->ifTrue(static fn ($v) => isset($v['dsn'], $v['dbal_connection'])) - ->thenInvalid('Either "dsn" or "dbal_connection" can be configured, but not both.') + ->stringNode('endpoint')->cannotBeEmpty()->end() + ->stringNode('api_key')->isRequired()->end() + ->stringNode('collection')->isRequired()->end() ->end() ->end() ->end() @@ -933,7 +938,7 @@ ->end() ->end() ->end() - ->arrayNode('surreal_db') + ->arrayNode('surrealdb') ->useAttributeAsKey('name') ->arrayPrototype() ->children() diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index a15fafe21..3953cd79e 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -79,10 +79,10 @@ use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore; use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickHouseStore; use Symfony\AI\Store\Bridge\Cloudflare\Store as CloudflareStore; -use Symfony\AI\Store\Bridge\Local\CacheStore; +use Symfony\AI\Store\Bridge\Local\CacheStore as LocalCacheStore; use Symfony\AI\Store\Bridge\Local\DistanceCalculator; use Symfony\AI\Store\Bridge\Local\DistanceStrategy; -use Symfony\AI\Store\Bridge\Local\InMemoryStore; +use Symfony\AI\Store\Bridge\Local\InMemoryStore as LocalInMemoryStore; use Symfony\AI\Store\Bridge\Manticore\Store as ManticoreStore; use Symfony\AI\Store\Bridge\MariaDb\Store as MariaDbStore; use Symfony\AI\Store\Bridge\Meilisearch\Store as MeilisearchStore; @@ -929,7 +929,7 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde * @param array $stores * @param array $setupStoresOptions */ - private function processStoreConfig(string $type, array $stores, ContainerBuilder $container, &$setupStoresOptions): void + private function processStoreConfig(string $type, array $stores, ContainerBuilder $container, array &$setupStoresOptions): void { if ('azure_search' === $type) { foreach ($stores as $name => $store) { @@ -947,8 +947,10 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $definition = new Definition(AzureSearchStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -961,11 +963,13 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $arguments = [ new Reference($store['service']), new Definition(DistanceCalculator::class), + $store['cache_key'] ?? $name, ]; if (\array_key_exists('strategy', $store) && null !== $store['strategy']) { if (!$container->hasDefinition('ai.store.distance_calculator.'.$name)) { $distanceCalculatorDefinition = new Definition(DistanceCalculator::class); + $distanceCalculatorDefinition->setLazy(true); $distanceCalculatorDefinition->setArgument(0, DistanceStrategy::from($store['strategy'])); $container->setDefinition('ai.store.distance_calculator.'.$name, $distanceCalculatorDefinition); @@ -974,13 +978,11 @@ 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 = new Definition(LocalCacheStore::class); $definition + ->setLazy(true) ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); @@ -993,10 +995,12 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde foreach ($stores as $name => $store) { $definition = new Definition(ChromaDbStore::class); $definition + ->setLazy(true) ->setArguments([ new Reference($store['client']), $store['collection'], ]) + ->addTag('proxy', ['interface' => StoreInterface::class]) ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); @@ -1012,20 +1016,21 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde } else { $httpClient = new Definition(HttpClientInterface::class); $httpClient + ->setLazy(true) ->setFactory([HttpClient::class, 'createForBaseUri']) - ->setArguments([$store['dsn']]) - ; + ->setArguments([$store['dsn']]); } $definition = new Definition(ClickHouseStore::class); $definition + ->setLazy(true) ->setArguments([ $httpClient, $store['database'], $store['table'], ]) - ->addTag('ai.store') - ; + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1040,24 +1045,20 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $store['account_id'], $store['api_key'], $store['index_name'], + $store['dimensions'], + $store['metric'], ]; - if (\array_key_exists('dimensions', $store)) { - $arguments[4] = $store['dimensions']; - } - - if (\array_key_exists('metric', $store)) { - $arguments[5] = $store['metric']; - } - - if (\array_key_exists('endpoint', $store)) { - $arguments[6] = $store['endpoint']; + if (\array_key_exists('endpoint_url', $store)) { + $arguments[6] = $store['endpoint_url']; } $definition = new Definition(CloudflareStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1071,32 +1072,22 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde new Reference('http_client'), $store['endpoint'], $store['table'], + $store['field'], + $store['type'], + $store['similarity'], + $store['dimensions'], ]; - if (\array_key_exists('field', $store)) { - $arguments[3] = $store['field']; - } - - if (\array_key_exists('type', $store)) { - $arguments[4] = $store['type']; - } - - if (\array_key_exists('similarity', $store)) { - $arguments[5] = $store['similarity']; - } - - if (\array_key_exists('dimensions', $store)) { - $arguments[6] = $store['dimensions']; - } - if (\array_key_exists('quantization', $store)) { $arguments[7] = $store['quantization']; } $definition = new Definition(ManticoreStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1106,25 +1097,24 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde if ('mariadb' === $type) { foreach ($stores as $name => $store) { - $arguments = [ - new Reference(\sprintf('doctrine.dbal.%s_connection', $store['connection'])), - $store['table_name'], - $store['index_name'], - $store['vector_field_name'], - ]; - $definition = new Definition(MariaDbStore::class); - $definition->setFactory([MariaDbStore::class, 'fromDbal']); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setFactory([MariaDbStore::class, 'fromDbal']) + ->setArguments([ + new Reference(\sprintf('doctrine.dbal.%s_connection', $store['connection'])), + $store['table_name'], + $store['index_name'], + $store['vector_field_name'], + ]) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); - $serviceId = 'ai.store.'.$type.'.'.$name; - $container->setDefinition($serviceId, $definition); - $container->registerAliasForArgument($serviceId, StoreInterface::class, $name); - $container->registerAliasForArgument($serviceId, StoreInterface::class, $type.'_'.$name); + $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); + $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); + $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $type.'_'.$name); - $setupStoresOptions[$serviceId] = $store['setup_options'] ?? []; + $setupStoresOptions['ai.store.'.$type.'.'.$name] = $store['setup_options'] ?? []; } } @@ -1135,28 +1125,21 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $store['endpoint'], $store['api_key'], $store['index_name'], + $store['embedder'], + $store['vector_field'], + $store['dimensions'], ]; - if (\array_key_exists('embedder', $store)) { - $arguments[4] = $store['embedder']; - } - - if (\array_key_exists('vector_field', $store)) { - $arguments[5] = $store['vector_field']; - } - - if (\array_key_exists('dimensions', $store)) { - $arguments[6] = $store['dimensions']; - } - if (\array_key_exists('semantic_ratio', $store)) { $arguments[7] = $store['semantic_ratio']; } $definition = new Definition(MeilisearchStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1166,11 +1149,14 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde if ('memory' === $type) { foreach ($stores as $name => $store) { - $arguments = []; + $arguments = [ + new Reference(DistanceCalculator::class), + ]; if (\array_key_exists('strategy', $store) && null !== $store['strategy']) { if (!$container->hasDefinition('ai.store.distance_calculator.'.$name)) { $distanceCalculatorDefinition = new Definition(DistanceCalculator::class); + $distanceCalculatorDefinition->setLazy(true); $distanceCalculatorDefinition->setArgument(0, DistanceStrategy::from($store['strategy'])); $container->setDefinition('ai.store.distance_calculator.'.$name, $distanceCalculatorDefinition); @@ -1179,10 +1165,12 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $arguments[0] = new Reference('ai.store.distance_calculator.'.$name); } - $definition = new Definition(InMemoryStore::class); + $definition = new Definition(LocalInMemoryStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1198,24 +1186,20 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $store['api_key'], $store['database'], $store['collection'], + $store['vector_field'], + $store['dimensions'], ]; - if (\array_key_exists('vector_field', $store)) { - $arguments[5] = $store['vector_field']; - } - - if (\array_key_exists('dimensions', $store)) { - $arguments[6] = $store['dimensions']; - } - if (\array_key_exists('metric_type', $store)) { $arguments[7] = $store['metric_type']; } $definition = new Definition(MilvusStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1230,20 +1214,19 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $store['database'], $store['collection'], $store['index_name'], + $store['vector_field'], ]; - if (\array_key_exists('vector_field', $store)) { - $arguments[4] = $store['vector_field']; - } - if (\array_key_exists('bulk_write', $store)) { $arguments[5] = $store['bulk_write']; } $definition = new Definition(MongoDbStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1261,28 +1244,21 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $store['database'], $store['vector_index_name'], $store['node_name'], + $store['vector_field'], + $store['dimensions'], + $store['distance'], ]; - if (\array_key_exists('vector_field', $store)) { - $arguments[7] = $store['vector_field']; - } - - if (\array_key_exists('dimensions', $store)) { - $arguments[8] = $store['dimensions']; - } - - if (\array_key_exists('distance', $store)) { - $arguments[9] = $store['distance']; - } - if (\array_key_exists('quantization', $store)) { $arguments[10] = $store['quantization']; } $definition = new Definition(Neo4jStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1295,20 +1271,61 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $arguments = [ new Reference($store['client']), $store['namespace'], + $store['filter'], ]; - if (\array_key_exists('filter', $store)) { - $arguments[2] = $store['filter']; - } - if (\array_key_exists('top_k', $store)) { $arguments[3] = $store['top_k']; } $definition = new Definition(PineconeStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); + + $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); + $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); + $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $type.'_'.$name); + } + } + + if ('postgres' === $type) { + foreach ($stores as $name => $store) { + $definition = new Definition(PostgresStore::class); + + if (\array_key_exists('dbal_connection', $store)) { + $definition->setFactory([PostgresStore::class, 'fromDbal']); + $arguments = [ + new Reference($store['dbal_connection']), + $store['table_name'], + $store['vector_field'], + ]; + } else { + $pdo = new Definition(\PDO::class); + $pdo->setArguments([ + $store['dsn'], + $store['username'] ?? null, + $store['password'] ?? null], + ); + + $arguments = [ + $pdo, + $store['table_name'], + $store['vector_field'], + ]; + } + + if (\array_key_exists('distance', $store)) { + $arguments[3] = $store['distance']; + } + + $definition + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1323,24 +1340,20 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $store['endpoint'], $store['api_key'], $store['collection_name'], + $store['dimensions'], + $store['distance'], ]; - if (\array_key_exists('dimensions', $store)) { - $arguments[4] = $store['dimensions']; - } - - if (\array_key_exists('distance', $store)) { - $arguments[5] = $store['distance']; - } - if (\array_key_exists('async', $store)) { $arguments[6] = $store['async']; } $definition = new Definition(QdrantStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1359,82 +1372,75 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $definition = new Definition(RedisStore::class); $definition - ->addTag('ai.store') + ->setLazy(true) ->setArguments([ $redisClient, $store['index_name'], $store['key_prefix'], $store['distance'], ]) - ; + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); + $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); + $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $type.'_'.$name); } } - if ('surreal_db' === $type) { + if ('supabase' === $type) { foreach ($stores as $name => $store) { $arguments = [ - new Reference('http_client'), - $store['endpoint'], - $store['username'], - $store['password'], - $store['namespace'], - $store['database'], + new Reference($store['http_client']), + $store['url'], + $store['api_key'], + $store['table'] ?? $name, + $store['vector_field'], + $store['vector_dimension'], ]; - if (\array_key_exists('table', $store)) { - $arguments[6] = $store['table']; - } - - if (\array_key_exists('vector_field', $store)) { - $arguments[7] = $store['vector_field']; - } - - if (\array_key_exists('strategy', $store)) { - $arguments[8] = $store['strategy']; - } - - if (\array_key_exists('dimensions', $store)) { - $arguments[9] = $store['dimensions']; - } - - if (\array_key_exists('namespaced_user', $store)) { - $arguments[10] = $store['namespaced_user']; + if (\array_key_exists('function_name', $store)) { + $arguments[6] = $store['function_name']; } - $definition = new Definition(SurrealDbStore::class); + $definition = new Definition(SupabaseStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); - $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); + $container->setDefinition('ai.store.supabase.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $type.'_'.$name); } } - if ('typesense' === $type) { + if ('surrealdb' === $type) { foreach ($stores as $name => $store) { $arguments = [ new Reference('http_client'), $store['endpoint'], - $store['api_key'], - $store['collection'], + $store['username'], + $store['password'], + $store['namespace'], + $store['database'], + $store['table'] ?? $name, + $store['vector_field'], + $store['strategy'], + $store['dimensions'], ]; - if (\array_key_exists('vector_field', $store)) { - $arguments[4] = $store['vector_field']; - } - - if (\array_key_exists('dimensions', $store)) { - $arguments[5] = $store['dimensions']; + if (\array_key_exists('namespaced_user', $store)) { + $arguments[10] = $store['namespaced_user']; } - $definition = new Definition(TypesenseStore::class); + $definition = new Definition(SurrealDbStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments($arguments) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1442,19 +1448,21 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde } } - if ('weaviate' === $type) { + if ('typesense' === $type) { foreach ($stores as $name => $store) { - $arguments = [ - new Reference('http_client'), - $store['endpoint'], - $store['api_key'], - $store['collection'], - ]; - - $definition = new Definition(WeaviateStore::class); + $definition = new Definition(TypesenseStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments([ + new Reference('http_client'), + $store['endpoint'], + $store['api_key'], + $store['collection'], + $store['vector_field'], + $store['dimensions'], + ]) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); @@ -1462,81 +1470,25 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde } } - if ('postgres' === $type) { + if ('weaviate' === $type) { foreach ($stores as $name => $store) { - $definition = new Definition(PostgresStore::class); - - if (\array_key_exists('dbal_connection', $store)) { - $definition->setFactory([PostgresStore::class, 'fromDbal']); - $arguments = [ - new Reference($store['dbal_connection']), - $store['table_name'], - ]; - } else { - $pdo = new Definition(\PDO::class); - $pdo->setArguments([ - $store['dsn'], - $store['username'] ?? null, - $store['password'] ?? null], - ); - - $arguments = [ - $pdo, - $store['table_name'], - ]; - } - - if (\array_key_exists('vector_field', $store)) { - $arguments[2] = $store['vector_field']; - } - - if (\array_key_exists('distance', $store)) { - $arguments[3] = $store['distance']; - } - + $definition = new Definition(WeaviateStore::class); $definition - ->addTag('ai.store') - ->setArguments($arguments); + ->setLazy(true) + ->setArguments([ + new Reference('http_client'), + $store['endpoint'], + $store['api_key'], + $store['collection'], + ]) + ->addTag('proxy', ['interface' => StoreInterface::class]) + ->addTag('ai.store'); $container->setDefinition('ai.store.'.$type.'.'.$name, $definition); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name); $container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $type.'_'.$name); } } - - if ('supabase' === $type) { - foreach ($stores as $name => $store) { - $arguments = [ - isset($store['http_client']) ? new Reference($store['http_client']) : new Definition(HttpClientInterface::class), - $store['url'], - $store['api_key'], - ]; - - if (\array_key_exists('table', $store)) { - $arguments[3] = $store['table']; - } - - if (\array_key_exists('vector_field', $store)) { - $arguments[4] = $store['vector_field']; - } - - if (\array_key_exists('vector_dimension', $store)) { - $arguments[5] = $store['vector_dimension']; - } - - if (\array_key_exists('function_name', $store)) { - $arguments[6] = $store['function_name']; - } - - $definition = new Definition(SupabaseStore::class); - $definition - ->addTag('ai.store') - ->setArguments($arguments); - - $container->setDefinition('ai.store.supabase.'.$name, $definition); - $container->registerAliasForArgument('ai.store.'.$name, StoreInterface::class, (new Target($name.'Store'))->getParsedName()); - } - } } /** @@ -1611,7 +1563,7 @@ private function processMessageStoreConfig(string $type, array $messageStores, C if ('memory' === $type) { foreach ($messageStores as $name => $messageStore) { - $definition = new Definition(InMemoryStore::class); + $definition = new Definition(LocalInMemoryStore::class); $definition ->setLazy(true) ->setArgument(0, $messageStore['identifier']) @@ -1709,7 +1661,7 @@ private function processMessageStoreConfig(string $type, array $messageStores, C } } - if ('surreal_db' === $type) { + if ('surrealdb' === $type) { foreach ($messageStores as $name => $messageStore) { $arguments = [ new Reference('http_client'), diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 239ef5cc0..546f96e0f 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -11,11 +11,13 @@ namespace Symfony\AI\AiBundle\Tests\DependencyInjection; +use Codewithkyrian\ChromaDB\Client as ChromaDbClient; use MongoDB\Client as MongoDbClient; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; +use Probots\Pinecone\Client as PineconeClient; use Symfony\AI\Agent\AgentInterface; use Symfony\AI\Agent\Memory\MemoryInputProcessor; use Symfony\AI\Agent\Memory\StaticMemoryProvider; @@ -27,6 +29,30 @@ use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog; use Symfony\AI\Platform\Capability; use Symfony\AI\Platform\Model; +use Symfony\AI\Store\Bridge\Azure\SearchStore as AzureStore; +use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore; +use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickhouseStore; +use Symfony\AI\Store\Bridge\Cloudflare\Store as CloudflareStore; +use Symfony\AI\Store\Bridge\Local\CacheStore as LocalCacheStore; +use Symfony\AI\Store\Bridge\Local\DistanceCalculator; +use Symfony\AI\Store\Bridge\Local\DistanceStrategy; +use Symfony\AI\Store\Bridge\Local\InMemoryStore as LocalInMemoryStoreAlias; +use Symfony\AI\Store\Bridge\Manticore\Store as ManticoreStore; +use Symfony\AI\Store\Bridge\MariaDb\Store as MariaDbStore; +use Symfony\AI\Store\Bridge\Meilisearch\Store as MeilisearchStore; +use Symfony\AI\Store\Bridge\Milvus\Store as MilvusStore; +use Symfony\AI\Store\Bridge\MongoDb\Store as MongoDbStore; +use Symfony\AI\Store\Bridge\Neo4j\Store as Neo4jStore; +use Symfony\AI\Store\Bridge\Pinecone\Store as PineconeStore; +use Symfony\AI\Store\Bridge\Postgres\Distance; +use Symfony\AI\Store\Bridge\Postgres\Store as PostgresStore; +use Symfony\AI\Store\Bridge\Qdrant\Store as QdrantStore; +use Symfony\AI\Store\Bridge\Redis\Distance as RedisDistance; +use Symfony\AI\Store\Bridge\Redis\Store as RedisStore; +use Symfony\AI\Store\Bridge\Supabase\Store as SupabaseStore; +use Symfony\AI\Store\Bridge\SurrealDb\Store as SurrealDbStore; +use Symfony\AI\Store\Bridge\Typesense\Store as TypesenseStore; +use Symfony\AI\Store\Bridge\Weaviate\Store as WeaviateStore; use Symfony\AI\Store\Document\Filter\TextContainsFilter; use Symfony\AI\Store\Document\Loader\InMemoryLoader; use Symfony\AI\Store\Document\Transformer\TextTrimTransformer; @@ -34,13 +60,13 @@ use Symfony\AI\Store\Document\VectorizerInterface; use Symfony\AI\Store\IndexerInterface; 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; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Translation\TranslatableMessage; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -385,168 +411,1944 @@ public function testAgentsAsToolsCannotDefineService() ]); } + public function testAzureStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'azure_search' => [ + 'my_azure_search_store' => [ + 'endpoint' => 'https://mysearch.search.windows.net', + 'api_key' => 'azure_search_key', + 'index_name' => 'my-documents', + 'api_version' => '2023-11-01', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.azure_search.my_azure_search_store')); + + $definition = $container->getDefinition('ai.store.azure_search.my_azure_search_store'); + $this->assertSame(AzureStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(5, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('https://mysearch.search.windows.net', $definition->getArgument(1)); + $this->assertSame('azure_search_key', $definition->getArgument(2)); + $this->assertSame('my-documents', $definition->getArgument(3)); + $this->assertSame('2023-11-01', $definition->getArgument(4)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_azure_search_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myAzureSearchStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $azureSearchMyAzureSearchStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testAzureStoreCanBeConfiguredWithCustomVectorField() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'azure_search' => [ + 'my_azure_search_store' => [ + 'endpoint' => 'https://mysearch.search.windows.net', + 'api_key' => 'azure_search_key', + 'index_name' => 'my-documents', + 'api_version' => '2023-11-01', + 'vector_field' => 'foo', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.azure_search.my_azure_search_store')); + + $definition = $container->getDefinition('ai.store.azure_search.my_azure_search_store'); + $this->assertSame(AzureStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(6, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('https://mysearch.search.windows.net', $definition->getArgument(1)); + $this->assertSame('azure_search_key', $definition->getArgument(2)); + $this->assertSame('my-documents', $definition->getArgument(3)); + $this->assertSame('2023-11-01', $definition->getArgument(4)); + $this->assertSame('foo', $definition->getArgument(5)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_azure_search_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myAzureSearchStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $azureSearchMyAzureSearchStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testCacheStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'cache' => [ + 'my_cache_store' => [ + 'service' => 'cache.system', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.cache.my_cache_store')); + $this->assertFalse($container->hasDefinition('ai.store.distance_calculator.my_cache_store')); + + $definition = $container->getDefinition('ai.store.cache.my_cache_store'); + $this->assertSame(LocalCacheStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(3, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('cache.system', (string) $definition->getArgument(0)); + $this->assertSame('my_cache_store', $definition->getArgument(2)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_cache_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myCacheStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $cacheMyCacheStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + public function testCacheStoreWithCustomKeyCanBeConfigured() { $container = $this->buildContainer([ 'ai' => [ 'store' => [ - 'cache' => [ - 'my_cache_store_with_custom_strategy' => [ - 'service' => 'cache.system', - 'cache_key' => 'random', + 'cache' => [ + 'my_cache_store_with_custom_key' => [ + 'service' => 'cache.system', + 'cache_key' => 'random', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.cache.my_cache_store_with_custom_key')); + $this->assertFalse($container->hasDefinition('ai.store.distance_calculator.my_cache_store_with_custom_key')); + + $definition = $container->getDefinition('ai.store.cache.my_cache_store_with_custom_key'); + $this->assertSame(LocalCacheStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $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->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $cache_my_cache_store_with_custom_key')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myCacheStoreWithCustomKey')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $cacheMyCacheStoreWithCustomKey')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testCacheStoreWithCustomStrategyCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'cache' => [ + 'my_cache_store_with_custom_strategy' => [ + 'service' => 'cache.system', + 'strategy' => 'chebyshev', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.cache.my_cache_store_with_custom_strategy')); + $this->assertTrue($container->hasDefinition('ai.store.distance_calculator.my_cache_store_with_custom_strategy')); + + $definition = $container->getDefinition('ai.store.cache.my_cache_store_with_custom_strategy'); + $this->assertSame(LocalCacheStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $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)); + + $strategyDefinition = $container->getDefinition('ai.store.distance_calculator.my_cache_store_with_custom_strategy'); + $this->assertTrue($strategyDefinition->isLazy()); + $this->assertSame(DistanceStrategy::CHEBYSHEV_DISTANCE, $strategyDefinition->getArgument(0)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $cache_my_cache_store_with_custom_strategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myCacheStoreWithCustomStrategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $cacheMyCacheStoreWithCustomStrategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testCacheStoreWithCustomStrategyAndKeyCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'cache' => [ + 'my_cache_store_with_custom_strategy_and_custom_key' => [ + 'service' => 'cache.system', + 'cache_key' => 'random', + 'strategy' => 'chebyshev', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.cache.my_cache_store_with_custom_strategy_and_custom_key')); + $this->assertTrue($container->hasDefinition('ai.store.distance_calculator.my_cache_store_with_custom_strategy_and_custom_key')); + + $definition = $container->getDefinition('ai.store.cache.my_cache_store_with_custom_strategy_and_custom_key'); + $this->assertSame(LocalCacheStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $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_and_custom_key', (string) $definition->getArgument(1)); + $this->assertSame('random', $definition->getArgument(2)); + + $strategyDefinition = $container->getDefinition('ai.store.distance_calculator.my_cache_store_with_custom_strategy_and_custom_key'); + $this->assertTrue($strategyDefinition->isLazy()); + $this->assertSame(DistanceStrategy::CHEBYSHEV_DISTANCE, $strategyDefinition->getArgument(0)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $cache_my_cache_store_with_custom_strategy_and_custom_key')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myCacheStoreWithCustomStrategyAndCustomKey')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $cacheMyCacheStoreWithCustomStrategyAndCustomKey')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testChromaDbStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'chroma_db' => [ + 'my_chroma_db_store' => [ + 'collection' => 'foo', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.chroma_db.my_chroma_db_store')); + + $definition = $container->getDefinition('ai.store.chroma_db.my_chroma_db_store'); + $this->assertSame(ChromaDbStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(2, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame(ChromaDbClient::class, (string) $definition->getArgument(0)); + $this->assertSame('foo', (string) $definition->getArgument(1)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $chroma_db_my_chroma_db_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myChromaDbStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $chromaDbMyChromaDbStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testChromaDbStoreWithCustomClientCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'chroma_db' => [ + 'my_chroma_db_store_with_custom_client' => [ + 'client' => 'bar', + 'collection' => 'foo', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.chroma_db.my_chroma_db_store_with_custom_client')); + + $definition = $container->getDefinition('ai.store.chroma_db.my_chroma_db_store_with_custom_client'); + $this->assertSame(ChromaDbStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(2, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('bar', (string) $definition->getArgument(0)); + $this->assertSame('foo', (string) $definition->getArgument(1)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $chroma_db_my_chroma_db_store_with_custom_client')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myChromaDbStoreWithCustomClient')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $chromaDbMyChromaDbStoreWithCustomClient')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testClickhouseStoreWithCustomHttpClientCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'clickhouse' => [ + 'my_clickhouse_store' => [ + 'http_client' => 'clickhouse.http_client', + 'database' => 'my_db', + 'table' => 'my_table', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.clickhouse.my_clickhouse_store')); + + $definition = $container->getDefinition('ai.store.clickhouse.my_clickhouse_store'); + $this->assertSame(ClickhouseStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(3, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('clickhouse.http_client', (string) $definition->getArgument(0)); + $this->assertSame('my_db', (string) $definition->getArgument(1)); + $this->assertSame('my_table', (string) $definition->getArgument(2)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_clickhouse_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myClickhouseStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $clickhouseMyClickhouseStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testClickhouseStoreWithCustomDsnCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'clickhouse' => [ + 'my_clickhouse_store' => [ + 'dsn' => 'http://foo:bar@1.2.3.4:9999', + 'database' => 'my_db', + 'table' => 'my_table', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.clickhouse.my_clickhouse_store')); + + $definition = $container->getDefinition('ai.store.clickhouse.my_clickhouse_store'); + $this->assertSame(ClickhouseStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(3, $definition->getArguments()); + $this->assertInstanceOf(Definition::class, $definition->getArgument(0)); + $this->assertSame(HttpClientInterface::class, $definition->getArgument(0)->getClass()); + $this->assertSame([HttpClient::class, 'createForBaseUri'], $definition->getArgument(0)->getFactory()); + $this->assertSame(['http://foo:bar@1.2.3.4:9999'], $definition->getArgument(0)->getArguments()); + $this->assertSame('my_db', (string) $definition->getArgument(1)); + $this->assertSame('my_table', (string) $definition->getArgument(2)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_clickhouse_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myClickhouseStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $clickhouseMyClickhouseStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testCloudflareStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'cloudflare' => [ + 'my_cloudflare_store' => [ + 'account_id' => 'foo', + 'api_key' => 'bar', + 'index_name' => 'random', + 'dimensions' => 1536, + 'metric' => 'cosine', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.cloudflare.my_cloudflare_store')); + + $definition = $container->getDefinition('ai.store.cloudflare.my_cloudflare_store'); + $this->assertSame(CloudflareStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(6, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('foo', $definition->getArgument(1)); + $this->assertSame('bar', $definition->getArgument(2)); + $this->assertSame('random', $definition->getArgument(3)); + $this->assertSame(1536, $definition->getArgument(4)); + $this->assertSame('cosine', $definition->getArgument(5)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $cloudflare_my_cloudflare_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myCloudflareStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $cloudflareMyCloudflareStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testCloudflareStoreWithCustomEndpointCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'cloudflare' => [ + 'my_cloudflare_store' => [ + 'account_id' => 'foo', + 'api_key' => 'bar', + 'index_name' => 'random', + 'dimensions' => 1536, + 'metric' => 'cosine', + 'endpoint_url' => 'https://api.cloudflare.com/client/v6/accounts', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.cloudflare.my_cloudflare_store')); + + $definition = $container->getDefinition('ai.store.cloudflare.my_cloudflare_store'); + $this->assertSame(CloudflareStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(7, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('foo', $definition->getArgument(1)); + $this->assertSame('bar', $definition->getArgument(2)); + $this->assertSame('random', $definition->getArgument(3)); + $this->assertSame(1536, $definition->getArgument(4)); + $this->assertSame('cosine', $definition->getArgument(5)); + $this->assertSame('https://api.cloudflare.com/client/v6/accounts', $definition->getArgument(6)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $cloudflare_my_cloudflare_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myCloudflareStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $cloudflareMyCloudflareStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testManticoreStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'manticore' => [ + 'my_manticore_store' => [ + 'endpoint' => 'http://127.0.0.1:9306', + 'table' => 'test', + 'field' => 'foo_vector', + 'type' => 'hnsw', + 'similarity' => 'cosine', + 'dimensions' => 768, + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.manticore.my_manticore_store')); + + $definition = $container->getDefinition('ai.store.manticore.my_manticore_store'); + $this->assertSame(ManticoreStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(7, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:9306', $definition->getArgument(1)); + $this->assertSame('test', $definition->getArgument(2)); + $this->assertSame('foo_vector', $definition->getArgument(3)); + $this->assertSame('hnsw', $definition->getArgument(4)); + $this->assertSame('cosine', $definition->getArgument(5)); + $this->assertSame(768, $definition->getArgument(6)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_manticore_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myManticoreStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $manticoreMyManticoreStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testManticoreStoreWithQuantizationCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'manticore' => [ + 'my_manticore_store' => [ + 'endpoint' => 'http://127.0.0.1:9306', + 'table' => 'test', + 'field' => 'foo_vector', + 'type' => 'hnsw', + 'similarity' => 'cosine', + 'dimensions' => 768, + 'quantization' => '1bit', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.manticore.my_manticore_store')); + + $definition = $container->getDefinition('ai.store.manticore.my_manticore_store'); + $this->assertSame(ManticoreStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(8, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:9306', $definition->getArgument(1)); + $this->assertSame('test', $definition->getArgument(2)); + $this->assertSame('foo_vector', $definition->getArgument(3)); + $this->assertSame('hnsw', $definition->getArgument(4)); + $this->assertSame('cosine', $definition->getArgument(5)); + $this->assertSame(768, $definition->getArgument(6)); + $this->assertSame('1bit', $definition->getArgument(7)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_manticore_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myManticoreStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $manticoreMyManticoreStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testMariaDbStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'mariadb' => [ + 'my_mariadb_store' => [ + 'connection' => 'default', + 'table_name' => 'vector_table', + 'index_name' => 'vector_idx', + 'vector_field_name' => 'vector', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.mariadb.my_mariadb_store')); + + $definition = $container->getDefinition('ai.store.mariadb.my_mariadb_store'); + $this->assertSame(MariaDbStore::class, $definition->getClass()); + $this->assertSame([MariaDbStore::class, 'fromDbal'], $definition->getFactory()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('doctrine.dbal.default_connection', (string) $definition->getArgument(0)); + $this->assertSame('vector_table', $definition->getArgument(1)); + $this->assertSame('vector_idx', $definition->getArgument(2)); + $this->assertSame('vector', $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_mariadb_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myMariadbStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mariadbMyMariadbStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testMariaDbStoreWithSetupOptionsCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'mariadb' => [ + 'my_mariadb_store' => [ + 'connection' => 'default', + 'table_name' => 'vector_table', + 'index_name' => 'vector_idx', + 'vector_field_name' => 'vector', + 'setup_options' => [ + 'dimensions' => 1024, + ], + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.mariadb.my_mariadb_store')); + + $definition = $container->getDefinition('ai.store.mariadb.my_mariadb_store'); + $this->assertSame(MariaDbStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('doctrine.dbal.default_connection', (string) $definition->getArgument(0)); + $this->assertSame('vector_table', $definition->getArgument(1)); + $this->assertSame('vector_idx', $definition->getArgument(2)); + $this->assertSame('vector', $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_mariadb_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myMariadbStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mariadbMyMariadbStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testMeilisearchMessageStoreIsConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'meilisearch' => [ + 'custom' => [ + 'endpoint' => 'http://127.0.0.1:7700', + 'api_key' => 'foo', + 'index_name' => 'test', + 'embedder' => 'default', + 'vector_field' => '_vectors', + 'dimensions' => 768, + ], + ], + ], + ], + ]); + + $definition = $container->getDefinition('ai.store.meilisearch.custom'); + $this->assertSame(MeilisearchStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(8, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:7700', $definition->getArgument(1)); + $this->assertSame('foo', $definition->getArgument(2)); + $this->assertSame('test', $definition->getArgument(3)); + $this->assertSame('default', $definition->getArgument(4)); + $this->assertSame('_vectors', $definition->getArgument(5)); + $this->assertSame(768, $definition->getArgument(6)); + $this->assertSame(1.0, $definition->getArgument(7)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $meilisearch_custom')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $custom')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $meilisearchCustom')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + #[TestDox('Meilisearch store with custom semantic_ratio can be configured')] + public function testMeilisearchStoreWithCustomSemanticRatioCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'meilisearch' => [ + 'test_store_with_semantic_ratio' => [ + 'endpoint' => 'http://127.0.0.1:7700', + 'api_key' => 'test_key', + 'index_name' => 'test_index', + 'embedder' => 'default', + 'vector_field' => '_vectors', + 'dimensions' => 768, + 'semantic_ratio' => 0.5, + ], + ], + ], + ], + ]); + + $definition = $container->getDefinition('ai.store.meilisearch.test_store_with_semantic_ratio'); + $this->assertSame(MeilisearchStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(8, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:7700', $definition->getArgument(1)); + $this->assertSame('test_key', $definition->getArgument(2)); + $this->assertSame('test_index', $definition->getArgument(3)); + $this->assertSame('default', $definition->getArgument(4)); + $this->assertSame('_vectors', $definition->getArgument(5)); + $this->assertSame(768, $definition->getArgument(6)); + $this->assertSame(0.5, $definition->getArgument(7)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $test_store_with_semantic_ratio')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $testStoreWithSemanticRatio')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $meilisearch_test_store_with_semantic_ratio')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $meilisearchTestStoreWithSemanticRatio')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testInMemoryStoreWithoutCustomStrategyCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'memory' => [ + 'my_memory_store_with_custom_strategy' => [], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.memory.my_memory_store_with_custom_strategy')); + + $definition = $container->getDefinition('ai.store.memory.my_memory_store_with_custom_strategy'); + $this->assertSame(LocalInMemoryStoreAlias::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(1, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame(DistanceCalculator::class, (string) $definition->getArgument(0)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_memory_store_with_custom_strategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myMemoryStoreWithCustomStrategy')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $memory_my_memory_store_with_custom_strategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $memoryMyMemoryStoreWithCustomStrategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testInMemoryStoreWithCustomStrategyCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'memory' => [ + 'my_memory_store_with_custom_strategy' => [ + 'strategy' => 'chebyshev', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.memory.my_memory_store_with_custom_strategy')); + $this->assertTrue($container->hasDefinition('ai.store.distance_calculator.my_memory_store_with_custom_strategy')); + + $definition = $container->getDefinition('ai.store.memory.my_memory_store_with_custom_strategy'); + $this->assertSame(LocalInMemoryStoreAlias::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(1, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('ai.store.distance_calculator.my_memory_store_with_custom_strategy', (string) $definition->getArgument(0)); + + $strategyDefinition = $container->getDefinition('ai.store.distance_calculator.my_memory_store_with_custom_strategy'); + $this->assertSame(DistanceStrategy::CHEBYSHEV_DISTANCE, $strategyDefinition->getArgument(0)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_memory_store_with_custom_strategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myMemoryStoreWithCustomStrategy')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $memory_my_memory_store_with_custom_strategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $memoryMyMemoryStoreWithCustomStrategy')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testMilvusStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'milvus' => [ + 'my_milvus_store' => [ + 'endpoint' => 'http://127.0.0.1:19530', + 'api_key' => 'foo', + 'database' => 'test', + 'collection' => 'default', + 'vector_field' => '_vectors', + 'dimensions' => 768, + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.milvus.my_milvus_store')); + + $definition = $container->getDefinition('ai.store.milvus.my_milvus_store'); + $this->assertSame(MilvusStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(7, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:19530', $definition->getArgument(1)); + $this->assertSame('foo', $definition->getArgument(2)); + $this->assertSame('test', $definition->getArgument(3)); + $this->assertSame('default', $definition->getArgument(4)); + $this->assertSame('_vectors', $definition->getArgument(5)); + $this->assertSame(768, $definition->getArgument(6)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_milvus_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myMilvusStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $milvus_my_milvus_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $milvusMyMilvusStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testMilvusStoreWithCustomMetricsCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'milvus' => [ + 'my_milvus_store' => [ + 'endpoint' => 'http://127.0.0.1:19530', + 'api_key' => 'foo', + 'database' => 'test', + 'collection' => 'default', + 'vector_field' => '_vectors', + 'dimensions' => 768, + 'metric_type' => 'COSINE', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.milvus.my_milvus_store')); + + $definition = $container->getDefinition('ai.store.milvus.my_milvus_store'); + $this->assertSame(MilvusStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(8, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:19530', $definition->getArgument(1)); + $this->assertSame('foo', $definition->getArgument(2)); + $this->assertSame('test', $definition->getArgument(3)); + $this->assertSame('default', $definition->getArgument(4)); + $this->assertSame('_vectors', $definition->getArgument(5)); + $this->assertSame(768, $definition->getArgument(6)); + $this->assertSame('COSINE', $definition->getArgument(7)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_milvus_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myMilvusStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $milvus_my_milvus_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $milvusMyMilvusStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testMongoDbStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'mongodb' => [ + 'my_mongo_store' => [ + 'database' => 'my_db', + 'collection' => 'my_collection', + 'index_name' => 'vector_index', + 'vector_field' => 'embedding', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.mongodb.my_mongo_store')); + + $definition = $container->getDefinition('ai.store.mongodb.my_mongo_store'); + $this->assertSame(MongoDbStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(5, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame(MongoDbClient::class, (string) $definition->getArgument(0)); + $this->assertSame('my_db', $definition->getArgument(1)); + $this->assertSame('my_collection', $definition->getArgument(2)); + $this->assertSame('vector_index', $definition->getArgument(3)); + $this->assertSame('embedding', $definition->getArgument(4)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_mongo_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myMongoStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $mongodb_my_mongo_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mongodbMyMongoStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testMongoDbStoreWithBulkWriteCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'mongodb' => [ + 'my_mongo_store' => [ + 'database' => 'my_db', + 'collection' => 'my_collection', + 'index_name' => 'vector_index', + 'vector_field' => 'embedding', + 'bulk_write' => true, + ], + ], + ], + ], + ]); + $this->assertTrue($container->hasDefinition('ai.store.mongodb.my_mongo_store')); + + $definition = $container->getDefinition('ai.store.mongodb.my_mongo_store'); + $this->assertSame(MongoDbStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(6, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame(MongoDbClient::class, (string) $definition->getArgument(0)); + $this->assertSame('my_db', $definition->getArgument(1)); + $this->assertSame('my_collection', $definition->getArgument(2)); + $this->assertSame('vector_index', $definition->getArgument(3)); + $this->assertSame('embedding', $definition->getArgument(4)); + $this->assertTrue($definition->getArgument(5)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_mongo_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myMongoStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $mongodb_my_mongo_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mongodbMyMongoStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testNeo4jStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'neo4j' => [ + 'my_neo4j_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'username' => 'test', + 'password' => 'test', + 'database' => 'foo', + 'vector_index_name' => 'test', + 'node_name' => 'foo', + 'vector_field' => '_vectors', + 'dimensions' => 768, + 'distance' => 'cosine', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.neo4j.my_neo4j_store')); + + $definition = $container->getDefinition('ai.store.neo4j.my_neo4j_store'); + $this->assertSame(Neo4jStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(10, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:8000', $definition->getArgument(1)); + $this->assertSame('test', $definition->getArgument(2)); + $this->assertSame('test', $definition->getArgument(3)); + $this->assertSame('foo', $definition->getArgument(4)); + $this->assertSame('test', $definition->getArgument(5)); + $this->assertSame('foo', $definition->getArgument(6)); + $this->assertSame('_vectors', $definition->getArgument(7)); + $this->assertSame(768, $definition->getArgument(8)); + $this->assertSame('cosine', $definition->getArgument(9)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_neo4j_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myNeo4jStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $neo4j_my_neo4j_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $neo4jMyNeo4jStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testNeo4jStoreWithQuantizationCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'neo4j' => [ + 'my_neo4j_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'username' => 'test', + 'password' => 'test', + 'database' => 'foo', + 'vector_index_name' => 'test', + 'node_name' => 'foo', + 'vector_field' => '_vectors', + 'dimensions' => 768, + 'distance' => 'cosine', + 'quantization' => true, + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.neo4j.my_neo4j_store')); + + $definition = $container->getDefinition('ai.store.neo4j.my_neo4j_store'); + $this->assertSame(Neo4jStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(11, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:8000', $definition->getArgument(1)); + $this->assertSame('test', $definition->getArgument(2)); + $this->assertSame('test', $definition->getArgument(3)); + $this->assertSame('foo', $definition->getArgument(4)); + $this->assertSame('test', $definition->getArgument(5)); + $this->assertSame('foo', $definition->getArgument(6)); + $this->assertSame('_vectors', $definition->getArgument(7)); + $this->assertSame(768, $definition->getArgument(8)); + $this->assertSame('cosine', $definition->getArgument(9)); + $this->assertTrue($definition->getArgument(10)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_neo4j_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myNeo4jStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $neo4j_my_neo4j_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $neo4jMyNeo4jStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testPineconeStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'pinecone' => [ + 'my_pinecone_store' => [ + 'namespace' => 'my_namespace', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.pinecone.my_pinecone_store')); + + $definition = $container->getDefinition('ai.store.pinecone.my_pinecone_store'); + $this->assertSame(PineconeStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(3, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame(PineconeClient::class, (string) $definition->getArgument(0)); + $this->assertSame('my_namespace', $definition->getArgument(1)); + $this->assertSame([], $definition->getArgument(2)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_pinecone_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myPineconeStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $pinecone_my_pinecone_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $pineconeMyPineconeStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testPineconeStoreWithFilterCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'pinecone' => [ + 'my_pinecone_store' => [ + 'namespace' => 'my_namespace', + 'filter' => ['category' => 'books'], + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.pinecone.my_pinecone_store')); + + $definition = $container->getDefinition('ai.store.pinecone.my_pinecone_store'); + $this->assertSame(PineconeStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(3, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame(PineconeClient::class, (string) $definition->getArgument(0)); + $this->assertSame('my_namespace', $definition->getArgument(1)); + $this->assertSame(['category' => 'books'], $definition->getArgument(2)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_pinecone_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myPineconeStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $pinecone_my_pinecone_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $pineconeMyPineconeStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testPineconeStoreWithTopKCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'pinecone' => [ + 'my_pinecone_store' => [ + 'namespace' => 'my_namespace', + 'top_k' => 10, + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.pinecone.my_pinecone_store')); + + $definition = $container->getDefinition('ai.store.pinecone.my_pinecone_store'); + $this->assertSame(PineconeStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame(PineconeClient::class, (string) $definition->getArgument(0)); + $this->assertSame('my_namespace', $definition->getArgument(1)); + $this->assertSame([], $definition->getArgument(2)); + $this->assertSame(10, $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_pinecone_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myPineconeStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $pinecone_my_pinecone_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $pineconeMyPineconeStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testPostgresStoreWithDifferentConnectionCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'postgres' => [ + 'db' => [ + 'dsn' => 'pgsql:host=localhost;port=5432;dbname=testdb;user=app;password=mypass', + 'table_name' => 'vectors', + 'vector_field' => 'foo', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.postgres.db')); + + $definition = $container->getDefinition('ai.store.postgres.db'); + $this->assertSame(PostgresStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Definition::class, $definition->getArgument(0)); + $this->assertSame(\PDO::class, $definition->getArgument(0)->getClass()); + $this->assertSame('vectors', $definition->getArgument(1)); + $this->assertSame('foo', $definition->getArgument(2)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $db')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $postgres_db')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $postgresDb')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'postgres' => [ + 'db' => [ + 'dsn' => 'pgsql:host=localhost;port=5432;dbname=testdb', + 'username' => 'foo', + 'password' => 'bar', + 'table_name' => 'vectors', + 'vector_field' => 'foo', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.postgres.db')); + + $definition = $container->getDefinition('ai.store.postgres.db'); + $this->assertSame(PostgresStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Definition::class, $definition->getArgument(0)); + $this->assertSame(\PDO::class, $definition->getArgument(0)->getClass()); + $this->assertSame(['pgsql:host=localhost;port=5432;dbname=testdb', 'foo', 'bar'], $definition->getArgument(0)->getArguments()); + $this->assertSame('vectors', $definition->getArgument(1)); + $this->assertSame('foo', $definition->getArgument(2)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $db')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $postgres_db')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $postgresDb')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'postgres' => [ + 'db' => [ + 'dbal_connection' => 'my_connection', + 'table_name' => 'vectors', + 'vector_field' => 'foo', + ], + ], + ], + ], + ]); + + $definition = $container->getDefinition('ai.store.postgres.db'); + $this->assertSame(PostgresStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('my_connection', (string) $definition->getArgument(0)); + $this->assertSame('vectors', $definition->getArgument(1)); + $this->assertSame('foo', $definition->getArgument(2)); + $this->assertSame(Distance::L2, $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $db')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $postgres_db')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $postgresDb')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'postgres' => [ + 'db' => [ + 'dbal_connection' => 'my_connection', + 'table_name' => 'vectors', + 'vector_field' => 'foo', + 'distance' => Distance::L1->value, + ], + ], + ], + ], + ]); + + $definition = $container->getDefinition('ai.store.postgres.db'); + $this->assertSame(PostgresStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('my_connection', (string) $definition->getArgument(0)); + $this->assertSame('vectors', $definition->getArgument(1)); + $this->assertSame('foo', $definition->getArgument(2)); + $this->assertSame(Distance::L1, $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $db')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $postgres_db')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $postgresDb')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testQdrantStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'qdrant' => [ + 'my_qdrant_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'api_key' => 'test', + 'collection_name' => 'foo', + 'dimensions' => 768, + 'distance' => 'Cosine', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.qdrant.my_qdrant_store')); + + $definition = $container->getDefinition('ai.store.qdrant.my_qdrant_store'); + $this->assertSame(QdrantStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(6, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:8000', $definition->getArgument(1)); + $this->assertSame('test', $definition->getArgument(2)); + $this->assertSame('foo', $definition->getArgument(3)); + $this->assertSame(768, $definition->getArgument(4)); + $this->assertSame('Cosine', $definition->getArgument(5)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_qdrant_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myQdrantStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $qdrant_my_qdrant_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $qdrantMyQdrantStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testQdrantStoreWithAsyncCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'qdrant' => [ + 'my_qdrant_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'api_key' => 'test', + 'collection_name' => 'foo', + 'dimensions' => 768, + 'distance' => 'Cosine', + 'async' => true, + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.qdrant.my_qdrant_store')); + + $definition = $container->getDefinition('ai.store.qdrant.my_qdrant_store'); + $this->assertSame(QdrantStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(7, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:8000', $definition->getArgument(1)); + $this->assertSame('test', $definition->getArgument(2)); + $this->assertSame('foo', $definition->getArgument(3)); + $this->assertSame(768, $definition->getArgument(4)); + $this->assertSame('Cosine', $definition->getArgument(5)); + $this->assertTrue($definition->getArgument(6)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_qdrant_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myQdrantStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $qdrant_my_qdrant_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $qdrantMyQdrantStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testRedisStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'redis' => [ + 'my_redis_store' => [ + 'connection_parameters' => [ + 'host' => '1.2.3.4', + 'port' => 6379, + ], + 'index_name' => 'my_vector_index', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.redis.my_redis_store')); + + $definition = $container->getDefinition('ai.store.redis.my_redis_store'); + $this->assertSame(RedisStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Definition::class, $definition->getArgument(0)); + $this->assertSame(\Redis::class, $definition->getArgument(0)->getClass()); + $this->assertSame('my_vector_index', $definition->getArgument(1)); + $this->assertSame('vector:', $definition->getArgument(2)); + $this->assertSame(RedisDistance::Cosine, $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_redis_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myRedisStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $redis_my_redis_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $redisMyRedisStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testRedisStoreWithCustomClientCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'redis' => [ + 'my_redis_store' => [ + 'client' => 'foo', + 'index_name' => 'my_vector_index', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.redis.my_redis_store')); + + $definition = $container->getDefinition('ai.store.redis.my_redis_store'); + $this->assertSame(RedisStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('foo', (string) $definition->getArgument(0)); + $this->assertSame('my_vector_index', $definition->getArgument(1)); + $this->assertSame('vector:', $definition->getArgument(2)); + $this->assertSame(RedisDistance::Cosine, $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_redis_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myRedisStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $redis_my_redis_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $redisMyRedisStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testRedisStoreWithCustomKeyPrefixCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'redis' => [ + 'my_redis_store' => [ + 'client' => 'foo', + 'index_name' => 'my_vector_index', + 'key_prefix' => 'foo:', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.redis.my_redis_store')); + + $definition = $container->getDefinition('ai.store.redis.my_redis_store'); + $this->assertSame(RedisStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('foo', (string) $definition->getArgument(0)); + $this->assertSame('my_vector_index', $definition->getArgument(1)); + $this->assertSame('foo:', $definition->getArgument(2)); + $this->assertSame(RedisDistance::Cosine, $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_redis_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myRedisStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $redis_my_redis_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $redisMyRedisStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testRedisStoreWithCustomDistanceCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'redis' => [ + 'my_redis_store' => [ + 'client' => 'foo', + 'index_name' => 'my_vector_index', + 'distance' => RedisDistance::L2, + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.redis.my_redis_store')); + + $definition = $container->getDefinition('ai.store.redis.my_redis_store'); + $this->assertSame(RedisStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('foo', (string) $definition->getArgument(0)); + $this->assertSame('my_vector_index', $definition->getArgument(1)); + $this->assertSame('vector:', $definition->getArgument(2)); + $this->assertSame(RedisDistance::L2, $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_redis_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myRedisStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $redis_my_redis_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $redisMyRedisStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + + public function testSupabaseStoreCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'supabase' => [ + 'my_supabase_store' => [ + 'url' => 'https://test.supabase.co', + 'api_key' => 'supabase_test_key', + 'table' => 'my_supabase_table', + 'vector_field' => 'my_embedding', + 'vector_dimension' => 1024, ], ], ], ], ]); - $this->assertTrue($container->hasDefinition('ai.store.cache.my_cache_store_with_custom_strategy')); - $this->assertFalse($container->hasDefinition('ai.store.distance_calculator.my_cache_store_with_custom_strategy')); + $this->assertTrue($container->hasDefinition('ai.store.supabase.my_supabase_store')); - $definition = $container->getDefinition('ai.store.cache.my_cache_store_with_custom_strategy'); + $definition = $container->getDefinition('ai.store.supabase.my_supabase_store'); + $this->assertSame(SupabaseStore::class, $definition->getClass()); - $this->assertCount(3, $definition->getArguments()); + $this->assertTrue($definition->isLazy()); + $this->assertCount(6, $definition->getArguments()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); - $this->assertSame('cache.system', (string) $definition->getArgument(0)); - $this->assertSame('random', $definition->getArgument(2)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('https://test.supabase.co', $definition->getArgument(1)); + $this->assertSame('supabase_test_key', $definition->getArgument(2)); + $this->assertSame('my_supabase_table', $definition->getArgument(3)); + $this->assertSame('my_embedding', $definition->getArgument(4)); + $this->assertSame(1024, $definition->getArgument(5)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_supabase_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mySupabaseStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $supabase_my_supabase_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $supabaseMySupabaseStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); } - public function testCacheStoreWithCustomStrategyCanBeConfigured() + public function testSupabaseStoreWithCustomHttpClientCanBeConfigured() { $container = $this->buildContainer([ 'ai' => [ 'store' => [ - 'cache' => [ - 'my_cache_store_with_custom_strategy' => [ - 'service' => 'cache.system', - 'strategy' => 'chebyshev', + 'supabase' => [ + 'my_supabase_store' => [ + 'http_client' => 'foo', + 'url' => 'https://test.supabase.co', + 'api_key' => 'supabase_test_key', + 'table' => 'my_supabase_table', + 'vector_field' => 'my_embedding', + 'vector_dimension' => 1024, ], ], ], ], ]); - $this->assertTrue($container->hasDefinition('ai.store.cache.my_cache_store_with_custom_strategy')); - $this->assertTrue($container->hasDefinition('ai.store.distance_calculator.my_cache_store_with_custom_strategy')); + $this->assertTrue($container->hasDefinition('ai.store.supabase.my_supabase_store')); - $definition = $container->getDefinition('ai.store.cache.my_cache_store_with_custom_strategy'); + $definition = $container->getDefinition('ai.store.supabase.my_supabase_store'); + $this->assertSame(SupabaseStore::class, $definition->getClass()); - $this->assertCount(3, $definition->getArguments()); + $this->assertTrue($definition->isLazy()); + $this->assertCount(6, $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)); + $this->assertSame('foo', (string) $definition->getArgument(0)); + $this->assertSame('https://test.supabase.co', $definition->getArgument(1)); + $this->assertSame('supabase_test_key', $definition->getArgument(2)); + $this->assertSame('my_supabase_table', $definition->getArgument(3)); + $this->assertSame('my_embedding', $definition->getArgument(4)); + $this->assertSame(1024, $definition->getArgument(5)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_supabase_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mySupabaseStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $supabase_my_supabase_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $supabaseMySupabaseStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); } - public function testCacheStoreWithCustomStrategyAndKeyCanBeConfigured() + public function testSupabaseStoreWithCustomFunctionCanBeConfigured() { $container = $this->buildContainer([ 'ai' => [ 'store' => [ - 'cache' => [ - 'my_cache_store_with_custom_strategy' => [ - 'service' => 'cache.system', - 'cache_key' => 'random', - 'strategy' => 'chebyshev', + 'supabase' => [ + 'my_supabase_store' => [ + 'url' => 'https://test.supabase.co', + 'api_key' => 'supabase_test_key', + 'table' => 'my_supabase_table', + 'vector_field' => 'my_embedding', + 'vector_dimension' => 1024, + 'function_name' => 'my_custom_function', ], ], ], ], ]); - $this->assertTrue($container->hasDefinition('ai.store.cache.my_cache_store_with_custom_strategy')); - $this->assertTrue($container->hasDefinition('ai.store.distance_calculator.my_cache_store_with_custom_strategy')); + $this->assertTrue($container->hasDefinition('ai.store.supabase.my_supabase_store')); - $definition = $container->getDefinition('ai.store.cache.my_cache_store_with_custom_strategy'); + $definition = $container->getDefinition('ai.store.supabase.my_supabase_store'); + $this->assertSame(SupabaseStore::class, $definition->getClass()); - $this->assertCount(3, $definition->getArguments()); + $this->assertTrue($definition->isLazy()); + $this->assertCount(7, $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('random', $definition->getArgument(2)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('https://test.supabase.co', $definition->getArgument(1)); + $this->assertSame('supabase_test_key', $definition->getArgument(2)); + $this->assertSame('my_supabase_table', $definition->getArgument(3)); + $this->assertSame('my_embedding', $definition->getArgument(4)); + $this->assertSame(1024, $definition->getArgument(5)); + $this->assertSame('my_custom_function', $definition->getArgument(6)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_supabase_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mySupabaseStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $supabase_my_supabase_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $supabaseMySupabaseStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); } - public function testInMemoryStoreWithoutCustomStrategyCanBeConfigured() + public function testSurrealDbStoreCanBeConfigured() { $container = $this->buildContainer([ 'ai' => [ 'store' => [ - 'memory' => [ - 'my_memory_store_with_custom_strategy' => [], + 'surrealdb' => [ + 'my_surrealdb_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'username' => 'test', + 'password' => 'test', + 'namespace' => 'foo', + 'database' => 'bar', + 'table' => 'bar', + 'vector_field' => '_vectors', + 'strategy' => 'cosine', + 'dimensions' => 768, + ], ], ], ], ]); - $this->assertTrue($container->hasDefinition('ai.store.memory.my_memory_store_with_custom_strategy')); + $this->assertTrue($container->hasDefinition('ai.store.surrealdb.my_surrealdb_store')); - $definition = $container->getDefinition('ai.store.memory.my_memory_store_with_custom_strategy'); - $this->assertCount(0, $definition->getArguments()); + $definition = $container->getDefinition('ai.store.surrealdb.my_surrealdb_store'); + $this->assertSame(SurrealDbStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(10, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:8000', $definition->getArgument(1)); + $this->assertSame('test', $definition->getArgument(2)); + $this->assertSame('test', $definition->getArgument(3)); + $this->assertSame('foo', $definition->getArgument(4)); + $this->assertSame('bar', $definition->getArgument(5)); + $this->assertSame('bar', $definition->getArgument(6)); + $this->assertSame('_vectors', $definition->getArgument(7)); + $this->assertSame('cosine', $definition->getArgument(8)); + $this->assertSame(768, $definition->getArgument(9)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_surrealdb_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mySurrealdbStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $surrealdb_my_surrealdb_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $surrealdbMySurrealdbStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); } - public function testInMemoryStoreWithCustomStrategyCanBeConfigured() + public function testSurrealDbStoreWithNamespacedUserCanBeConfigured() { $container = $this->buildContainer([ 'ai' => [ 'store' => [ - 'memory' => [ - 'my_memory_store_with_custom_strategy' => [ - 'strategy' => 'chebyshev', + 'surrealdb' => [ + 'my_surrealdb_store' => [ + 'endpoint' => 'http://127.0.0.1:8000', + 'username' => 'test', + 'password' => 'test', + 'namespace' => 'foo', + 'database' => 'bar', + 'table' => 'bar', + 'vector_field' => '_vectors', + 'strategy' => 'cosine', + 'dimensions' => 768, + 'namespaced_user' => true, ], ], ], ], ]); - $this->assertTrue($container->hasDefinition('ai.store.memory.my_memory_store_with_custom_strategy')); - $this->assertTrue($container->hasDefinition('ai.store.distance_calculator.my_memory_store_with_custom_strategy')); + $this->assertTrue($container->hasDefinition('ai.store.surrealdb.my_surrealdb_store')); - $definition = $container->getDefinition('ai.store.memory.my_memory_store_with_custom_strategy'); + $definition = $container->getDefinition('ai.store.surrealdb.my_surrealdb_store'); + $this->assertSame(SurrealDbStore::class, $definition->getClass()); - $this->assertCount(1, $definition->getArguments()); + $this->assertTrue($definition->isLazy()); + $this->assertCount(11, $definition->getArguments()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); - $this->assertSame('ai.store.distance_calculator.my_memory_store_with_custom_strategy', (string) $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://127.0.0.1:8000', $definition->getArgument(1)); + $this->assertSame('test', $definition->getArgument(2)); + $this->assertSame('test', $definition->getArgument(3)); + $this->assertSame('foo', $definition->getArgument(4)); + $this->assertSame('bar', $definition->getArgument(5)); + $this->assertSame('bar', $definition->getArgument(6)); + $this->assertSame('_vectors', $definition->getArgument(7)); + $this->assertSame('cosine', $definition->getArgument(8)); + $this->assertSame(768, $definition->getArgument(9)); + $this->assertTrue($definition->getArgument(10)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_surrealdb_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $mySurrealdbStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $surrealdb_my_surrealdb_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $surrealdbMySurrealdbStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); } - public function testPostgresStoreWithDifferentConnectionCanBeConfigured() + public function testTypesenseStoreCanBeConfigured() { $container = $this->buildContainer([ 'ai' => [ 'store' => [ - 'postgres' => [ - 'db' => [ - 'dsn' => 'pgsql:host=localhost;port=5432;dbname=testdb;user=app;password=mypass', - 'table_name' => 'vectors', + 'typesense' => [ + 'my_typesense_store' => [ + 'endpoint' => 'http://localhost:8108', + 'api_key' => 'foo', + 'collection' => 'my_collection', + 'vector_field' => 'vector', + 'dimensions' => 768, ], ], ], ], ]); - $this->assertTrue($container->hasDefinition('ai.store.postgres.db')); + $this->assertTrue($container->hasDefinition('ai.store.typesense.my_typesense_store')); - $definition = $container->getDefinition('ai.store.postgres.db'); - $this->assertCount(3, $definition->getArguments()); - $this->assertInstanceOf(Definition::class, $definition->getArgument(0)); + $definition = $container->getDefinition('ai.store.typesense.my_typesense_store'); + $this->assertSame(TypesenseStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(6, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://localhost:8108', $definition->getArgument(1)); + $this->assertSame('foo', $definition->getArgument(2)); + $this->assertSame('my_collection', $definition->getArgument(3)); + $this->assertSame('vector', $definition->getArgument(4)); + $this->assertSame(768, $definition->getArgument(5)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_typesense_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myTypesenseStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $typesense_my_typesense_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $typesenseMyTypesenseStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); + } + public function testWevaviateStoreCanBeConfigured() + { $container = $this->buildContainer([ 'ai' => [ 'store' => [ - 'postgres' => [ - 'db' => [ - 'dbal_connection' => 'my_connection', - 'table_name' => 'vectors', + 'weaviate' => [ + 'my_weaviate_store' => [ + 'endpoint' => 'http://localhost:8080', + 'api_key' => 'bar', + 'collection' => 'my_weaviate_collection', ], ], ], ], ]); - $definition = $container->getDefinition('ai.store.postgres.db'); - $this->assertCount(3, $definition->getArguments()); + $this->assertTrue($container->hasDefinition('ai.store.weaviate.my_weaviate_store')); + + $definition = $container->getDefinition('ai.store.weaviate.my_weaviate_store'); + $this->assertSame(WeaviateStore::class, $definition->getClass()); + + $this->assertTrue($definition->isLazy()); + $this->assertCount(4, $definition->getArguments()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame('http_client', (string) $definition->getArgument(0)); + $this->assertSame('http://localhost:8080', $definition->getArgument(1)); + $this->assertSame('bar', $definition->getArgument(2)); + $this->assertSame('my_weaviate_collection', $definition->getArgument(3)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $my_weaviate_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $myWeaviateStore')); + $this->assertTrue($container->hasAlias('.Symfony\AI\Store\StoreInterface $weaviate_my_weaviate_store')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface $weaviateMyWeaviateStore')); + $this->assertTrue($container->hasAlias('Symfony\AI\Store\StoreInterface')); } public function testConfigurationWithUseAttributeAsKeyWorksWithoutNormalizeKeys() @@ -576,6 +2378,7 @@ public function testConfigurationWithUseAttributeAsKeyWorksWithoutNormalizeKeys( 'database' => 'test_db', 'collection' => 'test_collection', 'index_name' => 'test_index', + 'vector_field' => 'foo', ], ], ], @@ -3078,63 +4881,6 @@ public function testDoctrineDbalMessageStoreWithCustomTableNameCanBeConfiguredWi $this->assertTrue($doctrineDbalDefaultMessageStoreDefinition->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->assertCount(5, $meilisearchMessageStoreDefinition->getArguments()); - $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->assertInstanceOf(Reference::class, $meilisearchMessageStoreDefinition->getArgument(4)); - $this->assertSame('serializer', (string) $meilisearchMessageStoreDefinition->getArgument(4)); - - $this->assertTrue($meilisearchMessageStoreDefinition->hasTag('proxy')); - $this->assertSame([['interface' => MessageStoreInterface::class]], $meilisearchMessageStoreDefinition->getTag('proxy')); - $this->assertTrue($meilisearchMessageStoreDefinition->hasTag('ai.message_store')); - } - - #[TestDox('Meilisearch store with custom semantic_ratio can be configured')] - public function testMeilisearchStoreWithCustomSemanticRatioCanBeConfigured() - { - $container = $this->buildContainer([ - 'ai' => [ - 'store' => [ - 'meilisearch' => [ - 'test_store' => [ - 'endpoint' => 'http://127.0.0.1:7700', - 'api_key' => 'test_key', - 'index_name' => 'test_index', - 'semantic_ratio' => 0.5, - ], - ], - ], - ], - ]); - - $this->assertTrue($container->hasDefinition('ai.store.meilisearch.test_store')); - $definition = $container->getDefinition('ai.store.meilisearch.test_store'); - $arguments = $definition->getArguments(); - $this->assertSame(0.5, $arguments[7]); - } - public function testMemoryMessageStoreCanBeConfiguredWithCustomKey() { $container = $this->buildContainer([ @@ -3348,7 +5094,7 @@ public function testSurrealDbMessageStoreIsConfiguredWithoutCustomTable() $container = $this->buildContainer([ 'ai' => [ 'message_store' => [ - 'surreal_db' => [ + 'surrealdb' => [ 'custom' => [ 'endpoint' => 'http://127.0.0.1:8000', 'username' => 'test', @@ -3361,7 +5107,7 @@ public function testSurrealDbMessageStoreIsConfiguredWithoutCustomTable() ], ]); - $surrealDbMessageStoreDefinition = $container->getDefinition('ai.message_store.surreal_db.custom'); + $surrealDbMessageStoreDefinition = $container->getDefinition('ai.message_store.surrealdb.custom'); $this->assertTrue($surrealDbMessageStoreDefinition->isLazy()); $this->assertCount(8, $surrealDbMessageStoreDefinition->getArguments()); @@ -3386,7 +5132,7 @@ public function testSurrealDbMessageStoreIsConfiguredWithCustomTable() $container = $this->buildContainer([ 'ai' => [ 'message_store' => [ - 'surreal_db' => [ + 'surrealdb' => [ 'custom' => [ 'endpoint' => 'http://127.0.0.1:8000', 'username' => 'test', @@ -3400,7 +5146,7 @@ public function testSurrealDbMessageStoreIsConfiguredWithCustomTable() ], ]); - $surrealDbMessageStoreDefinition = $container->getDefinition('ai.message_store.surreal_db.custom'); + $surrealDbMessageStoreDefinition = $container->getDefinition('ai.message_store.surrealdb.custom'); $this->assertTrue($surrealDbMessageStoreDefinition->isLazy()); $this->assertCount(8, $surrealDbMessageStoreDefinition->getArguments()); @@ -3425,7 +5171,7 @@ public function testSurrealDbMessageStoreIsConfiguredWithNamespacedUser() $container = $this->buildContainer([ 'ai' => [ 'message_store' => [ - 'surreal_db' => [ + 'surrealdb' => [ 'custom' => [ 'endpoint' => 'http://127.0.0.1:8000', 'username' => 'test', @@ -3439,7 +5185,7 @@ public function testSurrealDbMessageStoreIsConfiguredWithNamespacedUser() ], ]); - $surrealDbMessageStoreDefinition = $container->getDefinition('ai.message_store.surreal_db.custom'); + $surrealDbMessageStoreDefinition = $container->getDefinition('ai.message_store.surrealdb.custom'); $this->assertTrue($surrealDbMessageStoreDefinition->isLazy()); $this->assertCount(9, $surrealDbMessageStoreDefinition->getArguments()); @@ -3766,6 +5512,14 @@ private function getFullConfig(): array 'type' => 'hnsw', 'similarity' => 'cosine', 'dimensions' => 768, + ], + 'my_manticore_store_with_quantization' => [ + 'endpoint' => 'http://127.0.0.1:9306', + 'table' => 'test', + 'field' => 'foo_vector', + 'type' => 'hnsw', + 'similarity' => 'cosine', + 'dimensions' => 768, 'quantization' => '1bit', ], ], @@ -3836,6 +5590,24 @@ private function getFullConfig(): array 'filter' => ['category' => 'books'], 'top_k' => 10, ], + 'my_pinecone_store_with_filter' => [ + 'namespace' => 'my_namespace', + 'filter' => ['category' => 'books'], + ], + 'my_pinecone_store_with_top_k' => [ + 'namespace' => 'my_namespace', + 'filter' => ['category' => 'books'], + 'top_k' => 10, + ], + ], + 'postgres' => [ + 'my_postgres_store' => [ + 'dsn' => 'pgsql:host=127.0.0.1;port=5432;dbname=postgresql_db', + 'username' => 'postgres', + 'password' => 'pass', + 'table_name' => 'my_table', + 'vector_field' => 'my_embedding', + ], ], 'qdrant' => [ 'my_qdrant_store' => [ @@ -3851,17 +5623,21 @@ private function getFullConfig(): array 'api_key' => 'test', 'collection_name' => 'foo', 'dimensions' => 768, + 'distance' => 'Cosine', ], 'my_custom_distance_qdrant_store' => [ 'endpoint' => 'http://127.0.0.1:8000', 'api_key' => 'test', 'collection_name' => 'foo', + 'dimensions' => 768, 'distance' => 'Cosine', ], 'my_async_qdrant_store' => [ 'endpoint' => 'http://127.0.0.1:8000', 'api_key' => 'test', 'collection_name' => 'foo', + 'dimensions' => 768, + 'distance' => 'Cosine', 'async' => false, ], ], @@ -3873,9 +5649,49 @@ private function getFullConfig(): array ], 'index_name' => 'my_vector_index', ], + 'my_redis_store_with_custom_client' => [ + 'client' => 'foo', + 'index_name' => 'my_vector_index', + ], + 'my_redis_store_with_custom_key_prefix' => [ + 'client' => 'foo', + 'index_name' => 'my_vector_index', + 'key_prefix' => 'foo:', + ], + 'my_redis_store_with_custom_distance' => [ + 'client' => 'foo', + 'index_name' => 'my_vector_index', + 'distance' => RedisDistance::L2, + ], + ], + 'supabase' => [ + 'my_supabase_store' => [ + 'url' => 'https://test.supabase.co', + 'api_key' => 'supabase_test_key', + 'table' => 'my_supabase_table', + 'vector_field' => 'my_embedding', + 'vector_dimension' => 1024, + 'function_name' => 'my_match_function', + ], + 'my_supabase_store_with_custom_http_client' => [ + 'http_client' => 'foo', + 'url' => 'https://test.supabase.co', + 'api_key' => 'supabase_test_key', + 'table' => 'my_supabase_table', + 'vector_field' => 'my_embedding', + 'vector_dimension' => 1024, + ], + 'my_supabase_store_with_custom_function' => [ + 'url' => 'https://test.supabase.co', + 'api_key' => 'supabase_test_key', + 'table' => 'my_supabase_table', + 'vector_field' => 'my_embedding', + 'vector_dimension' => 1024, + 'function_name' => 'foo', + ], ], - 'surreal_db' => [ - 'my_surreal_db_store' => [ + 'surrealdb' => [ + 'my_surrealdb_store' => [ 'endpoint' => 'http://127.0.0.1:8000', 'username' => 'test', 'password' => 'test', @@ -3888,16 +5704,6 @@ private function getFullConfig(): array 'namespaced_user' => true, ], ], - 'supabase' => [ - 'my_supabase_store' => [ - 'url' => 'https://test.supabase.co', - 'api_key' => 'supabase_test_key', - 'table' => 'my_supabase_table', - 'vector_field' => 'my_embedding', - 'vector_dimension' => 1024, - 'function_name' => 'my_match_function', - ], - ], 'typesense' => [ 'my_typesense_store' => [ 'endpoint' => 'http://localhost:8108', @@ -3914,15 +5720,6 @@ private function getFullConfig(): array 'collection' => 'my_weaviate_collection', ], ], - 'postgres' => [ - 'my_postgres_store' => [ - 'dsn' => 'pgsql:host=127.0.0.1;port=5432;dbname=postgresql_db', - 'username' => 'postgres', - 'password' => 'pass', - 'table_name' => 'my_table', - 'vector_field' => 'my_embedding', - ], - ], ], 'message_store' => [ 'cache' => [ @@ -3986,8 +5783,8 @@ private function getFullConfig(): array 'identifier' => 'session', ], ], - 'surreal_db' => [ - 'my_surreal_db_message_store' => [ + 'surrealdb' => [ + 'my_surrealdb_message_store' => [ 'endpoint' => 'http://127.0.0.1:8000', 'username' => 'test', 'password' => 'test', @@ -3995,7 +5792,7 @@ private function getFullConfig(): array 'database' => 'bar', 'namespaced_user' => true, ], - 'my_surreal_db_message_store_with_custom_table' => [ + 'my_surrealdb_message_store_with_custom_table' => [ 'endpoint' => 'http://127.0.0.1:8000', 'username' => 'test', 'password' => 'test',