From c62b49aa0f425d04fb5a5d0730f86a0eaaa1ace1 Mon Sep 17 00:00:00 2001 From: Diego Aguiar Date: Thu, 18 Dec 2025 17:54:59 -0700 Subject: [PATCH 01/17] Add options arg to ManagedStoreInterface::drop() --- src/store/src/Bridge/Cache/Store.php | 2 +- src/store/src/Bridge/ClickHouse/Store.php | 2 +- src/store/src/Bridge/Cloudflare/Store.php | 2 +- src/store/src/Bridge/ManticoreSearch/Store.php | 2 +- src/store/src/Bridge/MariaDb/Store.php | 2 +- src/store/src/Bridge/Meilisearch/Store.php | 2 +- src/store/src/Bridge/Milvus/Store.php | 2 +- src/store/src/Bridge/MongoDb/Store.php | 2 +- src/store/src/Bridge/Neo4j/Store.php | 2 +- src/store/src/Bridge/OpenSearch/Store.php | 2 +- src/store/src/Bridge/Postgres/Store.php | 2 +- src/store/src/Bridge/Qdrant/Store.php | 2 +- src/store/src/Bridge/Redis/Store.php | 2 +- src/store/src/Bridge/SurrealDb/Store.php | 2 +- src/store/src/Bridge/Typesense/Store.php | 2 +- src/store/src/Bridge/Weaviate/Store.php | 2 +- src/store/src/InMemory/Store.php | 2 +- src/store/src/ManagedStoreInterface.php | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/store/src/Bridge/Cache/Store.php b/src/store/src/Bridge/Cache/Store.php index ce6e12a99..c994531c0 100644 --- a/src/store/src/Bridge/Cache/Store.php +++ b/src/store/src/Bridge/Cache/Store.php @@ -91,7 +91,7 @@ public function query(Vector $vector, array $options = []): iterable yield from $this->distanceCalculator->calculate($vectorDocuments, $vector, $options['maxItems'] ?? null); } - public function drop(): void + public function drop(array $options = []): void { $this->cache->clear(); } diff --git a/src/store/src/Bridge/ClickHouse/Store.php b/src/store/src/Bridge/ClickHouse/Store.php index 1b5a06623..dc2b783bc 100644 --- a/src/store/src/Bridge/ClickHouse/Store.php +++ b/src/store/src/Bridge/ClickHouse/Store.php @@ -48,7 +48,7 @@ public function setup(array $options = []): void $this->execute('POST', $sql); } - public function drop(): void + public function drop(array $options = []): void { $this->execute('POST', 'DROP TABLE IF EXISTS {{ table }}'); } diff --git a/src/store/src/Bridge/Cloudflare/Store.php b/src/store/src/Bridge/Cloudflare/Store.php index 446c161ba..8cd1e5943 100644 --- a/src/store/src/Bridge/Cloudflare/Store.php +++ b/src/store/src/Bridge/Cloudflare/Store.php @@ -52,7 +52,7 @@ public function setup(array $options = []): void ]); } - public function drop(): void + public function drop(array $options = []): void { $this->request('DELETE', \sprintf('vectorize/v2/indexes/%s', $this->index)); } diff --git a/src/store/src/Bridge/ManticoreSearch/Store.php b/src/store/src/Bridge/ManticoreSearch/Store.php index 1583d6e4f..6875b8d4b 100644 --- a/src/store/src/Bridge/ManticoreSearch/Store.php +++ b/src/store/src/Bridge/ManticoreSearch/Store.php @@ -50,7 +50,7 @@ public function setup(array $options = []): void )); } - public function drop(): void + public function drop(array $options = []): void { $this->request('cli', \sprintf('DROP TABLE %s', $this->table)); } diff --git a/src/store/src/Bridge/MariaDb/Store.php b/src/store/src/Bridge/MariaDb/Store.php index 0219fe8f1..3c9668ad1 100644 --- a/src/store/src/Bridge/MariaDb/Store.php +++ b/src/store/src/Bridge/MariaDb/Store.php @@ -76,7 +76,7 @@ public function setup(array $options = []): void ); } - public function drop(): void + public function drop(array $options = []): void { $this->connection->exec(\sprintf('DROP TABLE IF EXISTS %s', $this->tableName)); } diff --git a/src/store/src/Bridge/Meilisearch/Store.php b/src/store/src/Bridge/Meilisearch/Store.php index 6f59c7e81..4a3ce409e 100644 --- a/src/store/src/Bridge/Meilisearch/Store.php +++ b/src/store/src/Bridge/Meilisearch/Store.php @@ -101,7 +101,7 @@ public function query(Vector $vector, array $options = []): iterable } } - public function drop(): void + public function drop(array $options = []): void { $this->request('DELETE', \sprintf('indexes/%s', $this->indexName), []); } diff --git a/src/store/src/Bridge/Milvus/Store.php b/src/store/src/Bridge/Milvus/Store.php index 13dc16bc9..bd9549407 100644 --- a/src/store/src/Bridge/Milvus/Store.php +++ b/src/store/src/Bridge/Milvus/Store.php @@ -122,7 +122,7 @@ public function query(Vector $vector, array $options = []): iterable } } - public function drop(): void + public function drop(array $options = []): void { $this->request('POST', 'v2/vectordb/databases/drop', [ 'dbName' => $this->database, diff --git a/src/store/src/Bridge/MongoDb/Store.php b/src/store/src/Bridge/MongoDb/Store.php index 09d1c0b46..1d407c2d6 100644 --- a/src/store/src/Bridge/MongoDb/Store.php +++ b/src/store/src/Bridge/MongoDb/Store.php @@ -99,7 +99,7 @@ public function setup(array $options = []): void } } - public function drop(): void + public function drop(array $options = []): void { $this->getCollection()->drop(); } diff --git a/src/store/src/Bridge/Neo4j/Store.php b/src/store/src/Bridge/Neo4j/Store.php index 1662ddee2..8007e450d 100644 --- a/src/store/src/Bridge/Neo4j/Store.php +++ b/src/store/src/Bridge/Neo4j/Store.php @@ -79,7 +79,7 @@ public function query(Vector $vector, array $options = []): iterable } } - public function drop(): void + public function drop(array $options = []): void { $this->request('POST', \sprintf('db/%s/query/v2', $this->databaseName), [ 'statement' => 'MATCH (n) DETACH DELETE n', diff --git a/src/store/src/Bridge/OpenSearch/Store.php b/src/store/src/Bridge/OpenSearch/Store.php index 4fc658e8b..af9228b81 100644 --- a/src/store/src/Bridge/OpenSearch/Store.php +++ b/src/store/src/Bridge/OpenSearch/Store.php @@ -60,7 +60,7 @@ public function setup(array $options = []): void ]); } - public function drop(): void + public function drop(array $options = []): void { $indexExistResponse = $this->httpClient->request('HEAD', \sprintf('%s/%s', $this->endpoint, $this->indexName)); diff --git a/src/store/src/Bridge/Postgres/Store.php b/src/store/src/Bridge/Postgres/Store.php index 9294ca2ac..92001d04b 100644 --- a/src/store/src/Bridge/Postgres/Store.php +++ b/src/store/src/Bridge/Postgres/Store.php @@ -75,7 +75,7 @@ public function setup(array $options = []): void ); } - public function drop(): void + public function drop(array $options = []): void { $this->connection->exec(\sprintf('DROP TABLE IF EXISTS %s', $this->tableName)); } diff --git a/src/store/src/Bridge/Qdrant/Store.php b/src/store/src/Bridge/Qdrant/Store.php index 6d6e4bbbb..deb4d760c 100644 --- a/src/store/src/Bridge/Qdrant/Store.php +++ b/src/store/src/Bridge/Qdrant/Store.php @@ -103,7 +103,7 @@ public function query(Vector $vector, array $options = []): iterable } } - public function drop(): void + public function drop(array $options = []): void { $this->request('DELETE', \sprintf('collections/%s', $this->collectionName)); } diff --git a/src/store/src/Bridge/Redis/Store.php b/src/store/src/Bridge/Redis/Store.php index 85b43aa8e..e15c6569a 100644 --- a/src/store/src/Bridge/Redis/Store.php +++ b/src/store/src/Bridge/Redis/Store.php @@ -67,7 +67,7 @@ public function setup(array $options = []): void $this->redis->clearLastError(); } - public function drop(): void + public function drop(array $options = []): void { try { $this->redis->rawCommand('FT.DROPINDEX', $this->indexName); diff --git a/src/store/src/Bridge/SurrealDb/Store.php b/src/store/src/Bridge/SurrealDb/Store.php index 8c5da67c6..660edc95a 100644 --- a/src/store/src/Bridge/SurrealDb/Store.php +++ b/src/store/src/Bridge/SurrealDb/Store.php @@ -75,7 +75,7 @@ public function query(Vector $vector, array $options = []): iterable } } - public function drop(): void + public function drop(array $options = []): void { $this->authenticate(); diff --git a/src/store/src/Bridge/Typesense/Store.php b/src/store/src/Bridge/Typesense/Store.php index f501ef99b..16c44ca4d 100644 --- a/src/store/src/Bridge/Typesense/Store.php +++ b/src/store/src/Bridge/Typesense/Store.php @@ -86,7 +86,7 @@ public function query(Vector $vector, array $options = []): iterable } } - public function drop(): void + public function drop(array $options = []): void { $this->request('DELETE', \sprintf('collections/%s', $this->collection), []); } diff --git a/src/store/src/Bridge/Weaviate/Store.php b/src/store/src/Bridge/Weaviate/Store.php index 07efcccd2..787a1fb80 100644 --- a/src/store/src/Bridge/Weaviate/Store.php +++ b/src/store/src/Bridge/Weaviate/Store.php @@ -78,7 +78,7 @@ public function query(Vector $vector, array $options = []): iterable } } - public function drop(): void + public function drop(array $options = []): void { $this->request('DELETE', \sprintf('v1/schema/%s', $this->collection), []); } diff --git a/src/store/src/InMemory/Store.php b/src/store/src/InMemory/Store.php index f6eca29da..a46ad085f 100644 --- a/src/store/src/InMemory/Store.php +++ b/src/store/src/InMemory/Store.php @@ -65,7 +65,7 @@ public function query(Vector $vector, array $options = []): iterable yield from $this->distanceCalculator->calculate($documents, $vector, $options['maxItems'] ?? null); } - public function drop(): void + public function drop(array $options = []): void { $this->documents = []; } diff --git a/src/store/src/ManagedStoreInterface.php b/src/store/src/ManagedStoreInterface.php index 3a2f3c4b5..981d1e75e 100644 --- a/src/store/src/ManagedStoreInterface.php +++ b/src/store/src/ManagedStoreInterface.php @@ -21,5 +21,5 @@ interface ManagedStoreInterface */ public function setup(array $options = []): void; - public function drop(): void; + public function drop(array $options = []): void; } From 048584cb22578a410ec38c608f38cc21714e34e2 Mon Sep 17 00:00:00 2001 From: Diego Aguiar Date: Thu, 18 Dec 2025 18:11:48 -0700 Subject: [PATCH 02/17] add missing phpdoc block --- src/store/src/ManagedStoreInterface.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/store/src/ManagedStoreInterface.php b/src/store/src/ManagedStoreInterface.php index 981d1e75e..ac6399521 100644 --- a/src/store/src/ManagedStoreInterface.php +++ b/src/store/src/ManagedStoreInterface.php @@ -21,5 +21,8 @@ interface ManagedStoreInterface */ public function setup(array $options = []): void; + /** + * @param array $options + */ public function drop(array $options = []): void; } From e29c69578ad09eb5d88b253b661d8c2f8054aa53 Mon Sep 17 00:00:00 2001 From: Diego Aguiar Date: Thu, 18 Dec 2025 18:15:51 -0700 Subject: [PATCH 03/17] update Elasticsearch store --- src/store/src/Bridge/Elasticsearch/Store.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/src/Bridge/Elasticsearch/Store.php b/src/store/src/Bridge/Elasticsearch/Store.php index a5798d6f2..aed051f85 100644 --- a/src/store/src/Bridge/Elasticsearch/Store.php +++ b/src/store/src/Bridge/Elasticsearch/Store.php @@ -54,7 +54,7 @@ public function setup(array $options = []): void ]); } - public function drop(): void + public function drop(array $options = []): void { $indexExistResponse = $this->httpClient->request('HEAD', \sprintf('%s/%s', $this->endpoint, $this->indexName)); From 27bb9d337176dc8bf1c65a10f03166690ba25f50 Mon Sep 17 00:00:00 2001 From: Diego Aguiar Date: Thu, 18 Dec 2025 19:11:52 -0700 Subject: [PATCH 04/17] Implement Pinecone ManagedStore --- src/store/src/Bridge/Pinecone/Store.php | 37 ++++++++- .../src/Bridge/Pinecone/Tests/StoreTest.php | 83 ++++++++++++++++++- 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/store/src/Bridge/Pinecone/Store.php b/src/store/src/Bridge/Pinecone/Store.php index deabd8bbb..b063fefe1 100644 --- a/src/store/src/Bridge/Pinecone/Store.php +++ b/src/store/src/Bridge/Pinecone/Store.php @@ -16,25 +16,48 @@ use Symfony\AI\Platform\Vector\Vector; use Symfony\AI\Store\Document\Metadata; use Symfony\AI\Store\Document\VectorDocument; +use Symfony\AI\Store\ManagedStoreInterface; use Symfony\AI\Store\StoreInterface; use Symfony\Component\Uid\Uuid; /** * @author Christopher Hertel */ -final class Store implements StoreInterface +final class Store implements ManagedStoreInterface, StoreInterface { /** * @param array $filter */ public function __construct( private readonly Client $pinecone, + private readonly ?string $indexName = null, private readonly ?string $namespace = null, private readonly array $filter = [], private readonly int $topK = 3, ) { } + public function setup(array $options = []): void + { + if (null === $this->indexName) { + throw new \InvalidArgumentException('You need to configure and index name for the Pinecone store.'); + } + + if (false === isset($options['dimension'])) { + throw new \InvalidArgumentException('No supported options.'); + } + + $this->pinecone + ->control() + ->index($this->indexName) + ->createServerless( + $options['dimension'], + $options['metric'] ?? null, + $options['cloud'] ?? null, + $options['region'] ?? null, + ); + } + public function add(VectorDocument ...$documents): void { $vectors = []; @@ -73,6 +96,18 @@ public function query(Vector $vector, array $options = []): iterable } } + public function drop(array $options = []): void + { + if (null === $this->indexName) { + throw new \InvalidArgumentException('You need to configure and index name for the Pinecone store.'); + } + + $this->pinecone + ->control() + ->index($this->indexName) + ->delete(); + } + private function getVectors(): VectorResource { return $this->pinecone->data()->vectors(); diff --git a/src/store/src/Bridge/Pinecone/Tests/StoreTest.php b/src/store/src/Bridge/Pinecone/Tests/StoreTest.php index a3fb23bde..a4bf78e49 100644 --- a/src/store/src/Bridge/Pinecone/Tests/StoreTest.php +++ b/src/store/src/Bridge/Pinecone/Tests/StoreTest.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\TestCase; use Probots\Pinecone\Client; +use Probots\Pinecone\Resources\Control\IndexResource; +use Probots\Pinecone\Resources\ControlResource; use Probots\Pinecone\Resources\Data\VectorResource; use Probots\Pinecone\Resources\DataResource; use Saloon\Http\Response; @@ -24,6 +26,83 @@ final class StoreTest extends TestCase { + public function testStoreCantSetupWithInvalidOptions() + { + $pinecone = $this->createMock(Client::class); + $store = new Store($pinecone, 'text-index'); + + $this->expectException(\InvalidArgumentException::class); + + $store->setup(); + } + + public function testStoreCantSetupWithoutIndexName() + { + $pinecone = $this->createMock(Client::class); + $store = new Store($pinecone); + + $this->expectException(\InvalidArgumentException::class); + + $store->setup(['dimension' => 1536]); + } + + public function testStoreCanSetup() + { + $pinecone = $this->createMock(Client::class); + $indexResource = $this->createMock(IndexResource::class); + $controlResource = $this->createMock(ControlResource::class); + + $pinecone->expects($this->once()) + ->method('control') + ->willReturn($controlResource); + + $controlResource->expects($this->once()) + ->method('index') + ->with('test-index') + ->willReturn($indexResource); + + $indexResource->expects($this->once()) + ->method('createServerless') + ->with(1536, null, null, null); + + $store = new Store($pinecone, 'test-index'); + $store->setup([ + 'dimension' => 1536, + ]); + } + + public function testStoreCantDropWithInvalidOptions() + { + $pinecone = $this->createMock(Client::class); + $store = new Store($pinecone); + + $this->expectException(\InvalidArgumentException::class); + + $store->drop(); + } + + public function testStoreCanDrop() + { + $pinecone = $this->createMock(Client::class); + $indexResource = $this->createMock(IndexResource::class); + $controlResource = $this->createMock(ControlResource::class); + + $pinecone->expects($this->once()) + ->method('control') + ->willReturn($controlResource); + + $controlResource->expects($this->once()) + ->method('index') + ->with('test-index') + ->willReturn($indexResource); + + $indexResource->expects($this->once()) + ->method('delete'); + + $store = new Store($pinecone, 'test-index'); + $store->drop(); + } + public function testAddSingleDocument() { $vectorResource = $this->createMock(VectorResource::class); @@ -131,7 +210,7 @@ public function testAddWithNamespace() 'test-namespace', ); - $store = new Store($client, 'test-namespace'); + $store = new Store($client, null, 'test-namespace'); $document = new VectorDocument($uuid, new Vector([0.1, 0.2, 0.3])); $store->add($document); @@ -239,7 +318,7 @@ public function testQueryWithNamespaceAndFilter() ) ->willReturn($response); - $store = new Store($client, 'test-namespace', ['category' => 'test'], 5); + $store = new Store($client, null, 'test-namespace', ['category' => 'test'], 5); $results = iterator_to_array($store->query(new Vector([0.1, 0.2, 0.3]))); From 915ca28581488c993147bbf268d6b4ddee765b42 Mon Sep 17 00:00:00 2001 From: Diego Aguiar Date: Thu, 18 Dec 2025 19:12:20 -0700 Subject: [PATCH 05/17] Add bundle config for indexName property --- src/ai-bundle/config/options.php | 1 + src/ai-bundle/src/AiBundle.php | 3 +- .../DependencyInjection/AiBundleTest.php | 69 +++++++++++++++---- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 1a3396e44..56fa0c1df 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -800,6 +800,7 @@ ->cannotBeEmpty() ->defaultValue(PineconeClient::class) ->end() + ->stringNode('index_name')->end() ->stringNode('namespace')->end() ->arrayNode('filter') ->scalarPrototype() diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 355759f9f..b38ba54af 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -1519,12 +1519,13 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde foreach ($stores as $name => $store) { $arguments = [ new Reference($store['client']), + $store['index_name'] ?? null, $store['namespace'] ?? $name, $store['filter'], ]; if (\array_key_exists('top_k', $store)) { - $arguments[3] = $store['top_k']; + $arguments[4] = $store['top_k']; } $definition = new Definition(PineconeStore::class); diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index ed2157c22..f2d6e0d52 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -2387,11 +2387,12 @@ public function testPineconeStoreCanBeConfigured() $this->assertSame(PineconeStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); - $this->assertCount(3, $definition->getArguments()); + $this->assertCount(4, $definition->getArguments()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); $this->assertSame(PineconeClient::class, (string) $definition->getArgument(0)); - $this->assertSame('my_pinecone_store', $definition->getArgument(1)); - $this->assertSame([], $definition->getArgument(2)); + $this->assertNull($definition->getArgument(1)); + $this->assertSame('my_pinecone_store', $definition->getArgument(2)); + $this->assertSame([], $definition->getArgument(3)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); @@ -2424,11 +2425,12 @@ public function testPineconeStoreWithCustomNamespaceCanBeConfigured() $this->assertSame(PineconeStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); - $this->assertCount(3, $definition->getArguments()); + $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->assertNull($definition->getArgument(1)); + $this->assertSame('my_namespace', $definition->getArgument(2)); + $this->assertSame([], $definition->getArgument(3)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); @@ -2462,11 +2464,51 @@ public function testPineconeStoreWithCustomClientCanBeConfigured() $this->assertSame(PineconeStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); - $this->assertCount(3, $definition->getArguments()); + $this->assertCount(4, $definition->getArguments()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); $this->assertSame('foo', (string) $definition->getArgument(0)); - $this->assertSame('my_namespace', $definition->getArgument(1)); - $this->assertSame([], $definition->getArgument(2)); + $this->assertNull($definition->getArgument(1)); + $this->assertSame('my_namespace', $definition->getArgument(2)); + $this->assertSame([], $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 testPineconeStoreWithIndexNameCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'pinecone' => [ + 'my_pinecone_store' => [ + 'index_name' => 'my_index', + '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(4, $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); + $this->assertSame(PineconeClient::class, (string) $definition->getArgument(0)); + $this->assertSame('my_index', $definition->getArgument(1)); + $this->assertSame('my_namespace', $definition->getArgument(2)); + $this->assertSame([], $definition->getArgument(3)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); @@ -2500,12 +2542,13 @@ public function testPineconeStoreWithTopKCanBeConfigured() $this->assertSame(PineconeStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); - $this->assertCount(4, $definition->getArguments()); + $this->assertCount(5, $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(100, $definition->getArgument(3)); + $this->assertNull($definition->getArgument(1)); + $this->assertSame('my_namespace', $definition->getArgument(2)); + $this->assertSame([], $definition->getArgument(3)); + $this->assertSame(100, $definition->getArgument(4)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); From 8c2c774e6de5dac123974aa6ab7ccee112d4382b Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 19 Dec 2025 09:31:43 +0100 Subject: [PATCH 06/17] Remove redundant mongodb/mongodb conflict from examples The conflict is already declared in the required packages symfony/ai-mongo-db-store and symfony/ai-mongo-db-message-store. --- examples/composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/composer.json b/examples/composer.json index 96d4f9fce..6a507715c 100644 --- a/examples/composer.json +++ b/examples/composer.json @@ -138,9 +138,6 @@ "phpstan/phpstan": "^2.1", "phpstan/phpstan-strict-rules": "^2.0" }, - "conflict": { - "mongodb/mongodb": "<1.21" - }, "minimum-stability": "dev", "autoload": { "psr-4": { From 5697bbb4bcebd3b337b881ea370d29a819120896 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 19 Dec 2025 09:31:33 +0100 Subject: [PATCH 07/17] Add bridge type validation and fix message store types - Add validate-bridge-type.sh script to validate composer.json type field - Update validation workflow to run type validation for all bridge types - Fix message store bridges to use "symfony-ai-message-store" type --- .github/scripts/validate-bridge-type.sh | 61 +++++++++++++++++++ .github/workflows/validation.yaml | 14 +++++ src/chat/src/Bridge/Cache/composer.json | 2 +- src/chat/src/Bridge/Cloudflare/composer.json | 2 +- src/chat/src/Bridge/Doctrine/composer.json | 2 +- src/chat/src/Bridge/Meilisearch/composer.json | 2 +- src/chat/src/Bridge/MongoDb/composer.json | 2 +- src/chat/src/Bridge/Pogocache/composer.json | 2 +- src/chat/src/Bridge/Redis/composer.json | 2 +- src/chat/src/Bridge/Session/composer.json | 2 +- src/chat/src/Bridge/SurrealDb/composer.json | 2 +- 11 files changed, 84 insertions(+), 9 deletions(-) create mode 100755 .github/scripts/validate-bridge-type.sh diff --git a/.github/scripts/validate-bridge-type.sh b/.github/scripts/validate-bridge-type.sh new file mode 100755 index 000000000..e9d87df77 --- /dev/null +++ b/.github/scripts/validate-bridge-type.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Validates that all bridges have the correct "type" field in composer.json. +# +# Usage: validate-bridge-type.sh [component] +# +# Arguments: +# bridge_type Type of bridge (e.g., "store", "tool", "platform", "message-store") +# This determines the expected composer.json type: symfony-ai-{bridge_type} +# component Name of the parent component (e.g., agent, platform, store, chat) +# If not provided, defaults to bridge_type +# +# Example: +# validate-bridge-type.sh store +# validate-bridge-type.sh tool agent +# validate-bridge-type.sh message-store chat +# +# The script builds the bridge path internally as: src/${component}/src/Bridge/* + +set -e + +BRIDGE_TYPE="${1:?Bridge type is required (e.g., store, tool, platform, message-store)}" +COMPONENT="${2:-$BRIDGE_TYPE}" +BRIDGE_PATH="src/${COMPONENT}/src/Bridge/*" + +EXPECTED_TYPE="symfony-ai-${BRIDGE_TYPE}" +ERRORS=0 + +echo "Validating ${BRIDGE_TYPE} bridges have correct type (${BRIDGE_PATH})..." +echo "Expected type: ${EXPECTED_TYPE}" +echo "" + +for composer_file in ${BRIDGE_PATH}/composer.json; do + if [[ ! -f "$composer_file" ]]; then + continue + fi + + bridge_dir=$(dirname "$composer_file") + bridge_name=$(basename "$bridge_dir") + + actual_type=$(jq -r '.type // empty' "$composer_file") + + if [[ -z "$actual_type" ]]; then + echo "::error file=$composer_file::${BRIDGE_TYPE} bridge '$bridge_name' is missing 'type' field in composer.json" + ERRORS=$((ERRORS + 1)) + elif [[ "$actual_type" != "$EXPECTED_TYPE" ]]; then + echo "::error file=$composer_file::${BRIDGE_TYPE} bridge '$bridge_name' has incorrect type '$actual_type', expected '$EXPECTED_TYPE'" + ERRORS=$((ERRORS + 1)) + else + echo "✓ $bridge_name: type '$actual_type' is correct" + fi +done + +if [[ $ERRORS -gt 0 ]]; then + echo "" + echo "::error::Found $ERRORS type validation error(s) in ${BRIDGE_TYPE} bridges" + exit 1 +fi + +echo "" +echo "All ${BRIDGE_TYPE} bridges have the correct type!" diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index d0b0ae6c1..41e88c636 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -10,6 +10,7 @@ on: - '.github/scripts/validate-bridge-naming.sh' - '.github/scripts/validate-bridge-splitsh.sh' - '.github/scripts/validate-bridge-files.sh' + - '.github/scripts/validate-bridge-type.sh' pull_request: paths: - 'src/*/src/Bridge/**/*' @@ -19,6 +20,7 @@ on: - '.github/scripts/validate-bridge-naming.sh' - '.github/scripts/validate-bridge-splitsh.sh' - '.github/scripts/validate-bridge-files.sh' + - '.github/scripts/validate-bridge-type.sh' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -41,6 +43,9 @@ jobs: - name: Validate store bridges have required files run: .github/scripts/validate-bridge-files.sh store + - name: Validate store bridges have correct type + run: .github/scripts/validate-bridge-type.sh store + validate_tools: name: Tool Bridges runs-on: ubuntu-latest @@ -57,6 +62,9 @@ jobs: - name: Validate tool bridges have required files run: .github/scripts/validate-bridge-files.sh tool agent + - name: Validate tool bridges have correct type + run: .github/scripts/validate-bridge-type.sh tool agent + validate_message_stores: name: Message Store Bridges runs-on: ubuntu-latest @@ -73,6 +81,9 @@ jobs: - name: Validate message store bridges have required files run: .github/scripts/validate-bridge-files.sh message-store chat + - name: Validate message store bridges have correct type + run: .github/scripts/validate-bridge-type.sh message-store chat + validate_platforms: name: Platform Bridges runs-on: ubuntu-latest @@ -89,3 +100,6 @@ jobs: - name: Validate platform bridges have required files run: .github/scripts/validate-bridge-files.sh platform + - name: Validate platform bridges have correct type + run: .github/scripts/validate-bridge-type.sh platform + diff --git a/src/chat/src/Bridge/Cache/composer.json b/src/chat/src/Bridge/Cache/composer.json index 1d508bbd2..777e30c12 100644 --- a/src/chat/src/Bridge/Cache/composer.json +++ b/src/chat/src/Bridge/Cache/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-cache-message-store", "description": "Cache message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", diff --git a/src/chat/src/Bridge/Cloudflare/composer.json b/src/chat/src/Bridge/Cloudflare/composer.json index 99c0ab107..140f250f9 100644 --- a/src/chat/src/Bridge/Cloudflare/composer.json +++ b/src/chat/src/Bridge/Cloudflare/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-cloudflare-message-store", "description": "Cloudflare KV message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", diff --git a/src/chat/src/Bridge/Doctrine/composer.json b/src/chat/src/Bridge/Doctrine/composer.json index f854cf8ee..da03efce3 100644 --- a/src/chat/src/Bridge/Doctrine/composer.json +++ b/src/chat/src/Bridge/Doctrine/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-doctrine-message-store", "description": "Doctrine DBAL message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", diff --git a/src/chat/src/Bridge/Meilisearch/composer.json b/src/chat/src/Bridge/Meilisearch/composer.json index fa3fc96eb..0d426a053 100644 --- a/src/chat/src/Bridge/Meilisearch/composer.json +++ b/src/chat/src/Bridge/Meilisearch/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-meilisearch-message-store", "description": "Meilisearch message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", diff --git a/src/chat/src/Bridge/MongoDb/composer.json b/src/chat/src/Bridge/MongoDb/composer.json index 5e51e98e8..dfc14a7d5 100644 --- a/src/chat/src/Bridge/MongoDb/composer.json +++ b/src/chat/src/Bridge/MongoDb/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-mongo-db-message-store", "description": "MongoDB message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", diff --git a/src/chat/src/Bridge/Pogocache/composer.json b/src/chat/src/Bridge/Pogocache/composer.json index 8584f31c4..79f188fd7 100644 --- a/src/chat/src/Bridge/Pogocache/composer.json +++ b/src/chat/src/Bridge/Pogocache/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-pogocache-message-store", "description": "Pogocache message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", diff --git a/src/chat/src/Bridge/Redis/composer.json b/src/chat/src/Bridge/Redis/composer.json index acf8404b1..881b6c98e 100644 --- a/src/chat/src/Bridge/Redis/composer.json +++ b/src/chat/src/Bridge/Redis/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-redis-message-store", "description": "Redis message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", diff --git a/src/chat/src/Bridge/Session/composer.json b/src/chat/src/Bridge/Session/composer.json index 552e24593..23398f895 100644 --- a/src/chat/src/Bridge/Session/composer.json +++ b/src/chat/src/Bridge/Session/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-session-message-store", "description": "Symfony session message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", diff --git a/src/chat/src/Bridge/SurrealDb/composer.json b/src/chat/src/Bridge/SurrealDb/composer.json index 53407429d..7fadbd47f 100644 --- a/src/chat/src/Bridge/SurrealDb/composer.json +++ b/src/chat/src/Bridge/SurrealDb/composer.json @@ -2,7 +2,7 @@ "name": "symfony/ai-surreal-db-message-store", "description": "SurrealDB message store bridge for Symfony AI Chat", "license": "MIT", - "type": "symfony-ai-chat", + "type": "symfony-ai-message-store", "keywords": [ "ai", "bridge", From 208941ec97b8cd51be5e874421cfc94de84dcbd4 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 19 Dec 2025 09:58:31 +0100 Subject: [PATCH 08/17] Remove message store bridge path repositories The message store bridge packages now exist as proper packages, so the path repository entries are no longer needed. --- examples/composer.json | 36 ----------------------------------- src/ai-bundle/composer.json | 38 ------------------------------------- 2 files changed, 74 deletions(-) diff --git a/examples/composer.json b/examples/composer.json index 6a507715c..cfd53bdbd 100644 --- a/examples/composer.json +++ b/examples/composer.json @@ -19,42 +19,6 @@ { "type": "path", "url": "../src/store" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/Cache" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/Cloudflare" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/Doctrine" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/Session" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/Meilisearch" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/MongoDb" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/Pogocache" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/Redis" - }, - { - "type": "path", - "url": "../src/chat/src/Bridge/SurrealDb" } ], "require": { diff --git a/src/ai-bundle/composer.json b/src/ai-bundle/composer.json index 64cc86cd7..e12bccd59 100644 --- a/src/ai-bundle/composer.json +++ b/src/ai-bundle/composer.json @@ -3,44 +3,6 @@ "description": "Integration bundle for Symfony AI components", "license": "MIT", "type": "symfony-bundle", - "repositories": [ - { - "type": "path", - "url": "../chat/src/Bridge/Cache" - }, - { - "type": "path", - "url": "../chat/src/Bridge/Cloudflare" - }, - { - "type": "path", - "url": "../chat/src/Bridge/Doctrine" - }, - { - "type": "path", - "url": "../chat/src/Bridge/Meilisearch" - }, - { - "type": "path", - "url": "../chat/src/Bridge/MongoDb" - }, - { - "type": "path", - "url": "../chat/src/Bridge/Pogocache" - }, - { - "type": "path", - "url": "../chat/src/Bridge/Redis" - }, - { - "type": "path", - "url": "../chat/src/Bridge/Session" - }, - { - "type": "path", - "url": "../chat/src/Bridge/SurrealDb" - } - ], "authors": [ { "name": "Christopher Hertel", From d9a084072e908f806925e750dab4536f1195fc88 Mon Sep 17 00:00:00 2001 From: Eray Gundesli Date: Fri, 19 Dec 2025 11:10:40 +0100 Subject: [PATCH 09/17] [Platform] albert update models (#1212) --- examples/albert/chat.php | 2 +- examples/albert/embeddings.php | 2 +- src/platform/src/Bridge/Albert/ModelCatalog.php | 14 +++++++++++--- .../src/Bridge/Albert/Tests/ModelCatalogTest.php | 7 ++++--- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/albert/chat.php b/examples/albert/chat.php index a420da755..41722fcb3 100644 --- a/examples/albert/chat.php +++ b/examples/albert/chat.php @@ -39,6 +39,6 @@ Message::ofUser('Summarize the main objectives of France\'s AI strategy in one sentence.'), ); -$result = $platform->invoke('albert-small', $messages); +$result = $platform->invoke('openweight-small', $messages); echo $result->asText().\PHP_EOL; diff --git a/examples/albert/embeddings.php b/examples/albert/embeddings.php index dff0d611d..32086945a 100644 --- a/examples/albert/embeddings.php +++ b/examples/albert/embeddings.php @@ -15,7 +15,7 @@ $platform = PlatformFactory::create(env('ALBERT_API_KEY'), env('ALBERT_API_URL'), http_client()); -$response = $platform->invoke('embeddings-small', <<invoke('openweight-embeddings', << [ + 'openweight-small' => [ 'class' => CompletionsModel::class, 'capabilities' => [ Capability::INPUT_MESSAGES, @@ -36,7 +36,7 @@ public function __construct(array $additionalModels = []) Capability::OUTPUT_STREAMING, ], ], - 'albert-large' => [ + 'openweight-medium' => [ 'class' => CompletionsModel::class, 'capabilities' => [ Capability::INPUT_MESSAGES, @@ -44,7 +44,15 @@ public function __construct(array $additionalModels = []) Capability::OUTPUT_STREAMING, ], ], - 'embeddings-small' => [ + 'openweight-large' => [ + 'class' => CompletionsModel::class, + 'capabilities' => [ + Capability::INPUT_MESSAGES, + Capability::OUTPUT_TEXT, + Capability::OUTPUT_STREAMING, + ], + ], + 'openweight-embeddings' => [ 'class' => EmbeddingsModel::class, 'capabilities' => [Capability::INPUT_TEXT], ], diff --git a/src/platform/src/Bridge/Albert/Tests/ModelCatalogTest.php b/src/platform/src/Bridge/Albert/Tests/ModelCatalogTest.php index 6e3a3e8fc..77da71d61 100644 --- a/src/platform/src/Bridge/Albert/Tests/ModelCatalogTest.php +++ b/src/platform/src/Bridge/Albert/Tests/ModelCatalogTest.php @@ -25,9 +25,10 @@ final class ModelCatalogTest extends ModelCatalogTestCase { public static function modelsProvider(): iterable { - yield 'albert-small' => ['albert-small', CompletionsModel::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING]]; - yield 'albert-large' => ['albert-large', CompletionsModel::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING]]; - yield 'embeddings-small' => ['embeddings-small', EmbeddingsModel::class, [Capability::INPUT_TEXT]]; + yield 'openweight-small' => ['openweight-small', CompletionsModel::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING]]; + yield 'openweight-medium' => ['openweight-medium', CompletionsModel::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING]]; + yield 'openweight-large' => ['openweight-large', CompletionsModel::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING]]; + yield 'openweight-embeddings' => ['openweight-embeddings', EmbeddingsModel::class, [Capability::INPUT_TEXT]]; } protected function createModelCatalog(): ModelCatalogInterface From 07706360cbd9240d0459604c800a9213c3918b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 19 Dec 2025 12:20:29 +0100 Subject: [PATCH 10/17] [Doc][Agent] Fix typo --- docs/components/agent.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/components/agent.rst b/docs/components/agent.rst index 674e72d2c..34d2bfa8c 100644 --- a/docs/components/agent.rst +++ b/docs/components/agent.rst @@ -37,7 +37,7 @@ array of options:: // Platform instantiation $agent = new Agent($platform, $model); - $input = new MessageBag( + $messages = new MessageBag( Message::forSystem('You are a helpful chatbot answering questions about LLM agent.'), Message::ofUser('Hello, how are you?'), ); From 8d45d9559ffa20b5aa2c8560512688ed823809b0 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Fri, 19 Dec 2025 10:33:35 +0100 Subject: [PATCH 11/17] Remove azure bridge from demo --- demo/composer.json | 1 - demo/config/reference.php | 15 ++++++++++++++- src/ai-bundle/src/AiBundle.php | 16 ++++++++-------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/demo/composer.json b/demo/composer.json index ea0635864..5e0f1701a 100644 --- a/demo/composer.json +++ b/demo/composer.json @@ -12,7 +12,6 @@ "mrmysql/youtube-transcript": "^0.0.5", "nyholm/psr7": "^1.8", "php-http/discovery": "^1.20", - "symfony/ai-azure-platform": "@dev", "symfony/ai-bundle": "@dev", "symfony/ai-chroma-db-store": "@dev", "symfony/ai-clock-tool": "@dev", diff --git a/demo/config/reference.php b/demo/config/reference.php index 65c259f48..778b65d41 100644 --- a/demo/config/reference.php +++ b/demo/config/reference.php @@ -159,10 +159,16 @@ * version: string, * http_client?: string, // Service ID of the HTTP client to use // Default: "http_client" * }, + * decart?: array{ + * api_key: string, + * host?: string, // Default: "https://api.decart.ai/v1" + * http_client?: string, // Service ID of the HTTP client to use // Default: "http_client" + * }, * elevenlabs?: array{ * api_key: string, * host?: string, // Default: "https://api.elevenlabs.io/v1" * http_client?: string, // Service ID of the HTTP client to use // Default: "http_client" + * api_catalog?: bool, // If set, the ElevenLabs API will be used to build the catalog and retrieve models information, using this option leads to additional HTTP calls * }, * gemini?: array{ * api_key: string, @@ -236,7 +242,6 @@ * }>>, * agent?: array, + * elasticsearch?: array, * opensearch?: array $config) { + if (!ContainerBuilder::willBeAvailable('symfony/ai-azure-platform', AzureOpenAiPlatformFactory::class, ['symfony/ai-bundle'])) { + throw new RuntimeException('Azure platform configuration requires "symfony/ai-azure-platform" package. Try running "composer require symfony/ai-azure-platform".'); + } + $platformId = 'ai.platform.azure.'.$name; $definition = (new Definition(Platform::class)) ->setFactory(AzureOpenAiPlatformFactory::class.'::create') @@ -536,11 +536,11 @@ private function processPlatformConfig(string $type, array $platform, ContainerB } if ('generic' === $type) { - if (!ContainerBuilder::willBeAvailable('symfony/ai-generic-platform', GenericPlatformFactory::class, ['symfony/ai-bundle'])) { - throw new RuntimeException('Generic platform configuration requires "symfony/ai-generic-platform" package. Try running "composer require symfony/ai-generic-platform".'); - } - foreach ($platform as $name => $config) { + if (!ContainerBuilder::willBeAvailable('symfony/ai-generic-platform', GenericPlatformFactory::class, ['symfony/ai-bundle'])) { + throw new RuntimeException('Generic platform configuration requires "symfony/ai-generic-platform" package. Try running "composer require symfony/ai-generic-platform".'); + } + $platformId = 'ai.platform.generic.'.$name; $definition = (new Definition(Platform::class)) ->setFactory(GenericPlatformFactory::class.'::create') From 9c058de7c872af4bed9af14c609734aab406fe7b Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 19 Dec 2025 11:56:42 +0100 Subject: [PATCH 12/17] Simplify all component and bundle CHANGELOG.md files Reduce each changelog to just version 0.1 with a single entry: - "Add the component" for components (agent, platform, store, chat) - "Add the bundle" for bundles (ai-bundle, mcp-bundle) - Fix OpenSearch bridge changelog to use consistent format --- src/agent/CHANGELOG.md | 54 +--------------- src/ai-bundle/CHANGELOG.md | 31 +--------- src/chat/CHANGELOG.md | 11 +--- src/mcp-bundle/CHANGELOG.md | 21 +------ src/platform/CHANGELOG.md | 65 +------------------- src/store/CHANGELOG.md | 62 +------------------ src/store/src/Bridge/OpenSearch/CHANGELOG.md | 2 +- 7 files changed, 7 insertions(+), 239 deletions(-) diff --git a/src/agent/CHANGELOG.md b/src/agent/CHANGELOG.md index 7a7144e97..86009a85a 100644 --- a/src/agent/CHANGELOG.md +++ b/src/agent/CHANGELOG.md @@ -4,56 +4,4 @@ CHANGELOG 0.1 --- - * Add support for union types and polymorphic types via DiscriminatorMap - * Add Agent class as central orchestrator for AI interactions through the Platform component - * Add input/output processing pipeline: - - `InputProcessorInterface` for pre-processing messages and options - - `OutputProcessorInterface` for post-processing LLM responses - - `AgentAwareInterface` for processors requiring agent access - - `SystemPromptInputProcessor` for system prompt injection - - `ModelOverrideInputProcessor` for dynamic model switching - * Add comprehensive tool system: - - `#[AsTool]` attribute for simple tool registration - - `ReflectionToolFactory` for auto-discovering tools with attributes - - `MemoryToolFactory` for manual tool registration - - `ChainFactory` for combining multiple factories - - Automatic JSON Schema generation for parameter validation - - Tool call execution with argument resolution - - `ToolCallsExecuted` and `ToolCallArgumentsResolved` events - - `FaultTolerantToolbox` for graceful error handling - * Add built-in tools: - - `SimilaritySearch` for RAG/vector store searches - - `Agent` allowing agents to use other agents as tools - - `Clock` for current date/time - - `Crawler` for web page crawling - - `Mapbox` for geocoding addresses to coordinates and reverse geocoding - - `OpenMeteo` for weather information - - `SerpApi` for search engine results - - `Wikipedia` for Wikipedia content retrieval - - `YouTubeTranscriber` for YouTube video transcription - * Add bridges: - - `Brave` for web search integration (`symfony/ai-brave-tool`) - - `Tavily` for AI-powered search (`symfony/ai-tavily-tool`) - * Add structured output support: - - PHP class output with automatic conversion from LLM responses - - Array structure output with JSON schema validation - - `ResponseFormatFactory` for schema generation - - Symfony Serializer integration - * Add memory management system: - - `MemoryInputProcessor` for injecting contextual memory - - `StaticMemoryProvider` for fixed contextual information - - `EmbeddingProvider` for vector-based memory retrieval - - Dynamic memory control with `use_memory` option - - Extensible `MemoryInterface` for custom providers - * Add advanced features: - - Tool filtering to limit available tools per agent call - - Tool message retention option for context preservation - - Multi-method tools support in single classes - - Tool parameter validation with `#[With]` attribute - - Stream response support for real-time output - - PSR-3 logger integration throughout - - Symfony EventDispatcher integration - * Add model capability detection before processing - * Add comprehensive type safety with full PHP type hints - * Add clear exception hierarchy for different error scenarios - * Add translation support for system prompts + * Add the component diff --git a/src/ai-bundle/CHANGELOG.md b/src/ai-bundle/CHANGELOG.md index f85ed1f4a..5285ba6d7 100644 --- a/src/ai-bundle/CHANGELOG.md +++ b/src/ai-bundle/CHANGELOG.md @@ -4,33 +4,4 @@ CHANGELOG 0.1 --- - * Add Symfony bundle for integrating Platform, Agent, and Store components - * Add memory provider configuration support for agents. Symfony AI was bootstrapped by Christopher Hertel and Oskar Stark - * Add Perplexity platform support with API key configuration - * Add service configuration: - - Agent services with configurable platforms and system prompts - - Tool registration via `#[AsTool]` attribute and `ai.tool` tag - - Input/Output processor registration via `ai.agent.input_processor` and `ai.agent.output_processor` tags - - Abstract service definitions for extensibility - * Add Symfony Profiler integration for monitoring AI interactions - * Add security integration: - - `#[IsGrantedTool]` attribute for tool-level authorization - - Security voter integration for runtime permission checks - * Add configuration options: - - Multiple agents configuration with different platforms - - Platform credentials (API keys, endpoints) - - Model configurations per agent - - Vector store configurations - * Add dependency injection integration: - - Autoconfiguration for tools and processors - - Service aliases for default agent and platform - - Factory services for creating platforms - * Add bundle configuration with semantic validation - * Add support for fault-tolerant tool execution - * Add structured output configuration support - * Add token usage tracking: - - `track_token_usage` option for agents to monitor AI model consumption - - Automatic registration of token output processors for Mistral, OpenAI and Vertex AI - - Token usage metadata in agent results including prompt, completion, total, cached, and thinking tokens - - Rate limit information tracking for supported platforms - * Add support for configuring chats and message stores + * Add the bundle diff --git a/src/chat/CHANGELOG.md b/src/chat/CHANGELOG.md index 13e6d2767..86009a85a 100644 --- a/src/chat/CHANGELOG.md +++ b/src/chat/CHANGELOG.md @@ -4,13 +4,4 @@ CHANGELOG 0.1 --- - * Introduce the component - * Add support for external message stores: - - Symfony Cache - - Cloudflare - - Doctrine - - Meilisearch - - MongoDb - - Pogocache - - Redis - - SurrealDb + * Add the component diff --git a/src/mcp-bundle/CHANGELOG.md b/src/mcp-bundle/CHANGELOG.md index 4c3b2cd72..5285ba6d7 100644 --- a/src/mcp-bundle/CHANGELOG.md +++ b/src/mcp-bundle/CHANGELOG.md @@ -4,23 +4,4 @@ CHANGELOG 0.1 --- - * Add Symfony bundle providing Model Context Protocol integration using official `mcp/sdk` - * Add server mode exposing MCP capabilities to clients: - - STDIO transport via `php bin/console mcp:server` command - - HTTP transport via StreamableHttpTransport using configurable endpoints - - Automatic capability discovery and registration - - EventDispatcher integration for capability change notifications - * Add configurable HTTP transport features: - - Configurable endpoint path (default: `/_mcp`) - - File and memory session store options - - TTL configuration for session management - - CORS headers for cross-origin requests - * Add `McpController` for handling HTTP transport connections - * Add `McpCommand` providing STDIO interface - * Add bundle configuration for transport selection and HTTP options - * Add dedicated MCP logger with configurable Monolog integration - * Add pagination and instructions configuration - * Tools using `#[McpTool]` attribute automatically discovered - * Prompts using `#[McpPrompt]` attribute automatically discovered - * Resources using `#[McpResource]` attribute automatically discovered - * Resource templates using `#[McpResourceTemplate]` attribute automatically discovered + * Add the bundle diff --git a/src/platform/CHANGELOG.md b/src/platform/CHANGELOG.md index 3bf4557d3..86009a85a 100644 --- a/src/platform/CHANGELOG.md +++ b/src/platform/CHANGELOG.md @@ -4,67 +4,4 @@ CHANGELOG 0.1 --- - * Add nullables as required in structured outputs - * Add support for Albert API for French/EU data sovereignty - * Add unified abstraction layer for interacting with various AI models and providers - * Add support for 16+ AI providers: - - OpenAI (GPT-4, GPT-3.5, DALL·E, Whisper) - - Anthropic (Claude models via native API and AWS Bedrock) - - Google (VertexAi and Gemini models with server-side tools support) - - Azure (OpenAI and Meta Llama models) - - AWS Bedrock (Anthropic Claude, Meta Llama, Amazon Nova) - - Mistral AI (language models and embeddings) - - Meta Llama (via Azure, Ollama, Replicate, AWS Bedrock) - - Ollama (local model hosting) - - Replicate (cloud-based model hosting) - - OpenRouter (Google Gemini, DeepSeek R1) - - Voyage AI (specialized embeddings) - - HuggingFace (extensive model support with multiple tasks) - - TransformersPHP (local PHP-based transformer models) - - LM Studio (local model hosting) - - Cerebras (language models like Llama 4, Qwen 3, and more) - - Perplexity (Sonar models, supporting search results) - - AI/ML API (language models and embeddings) - - Docker Model Runner (local model hosting) - - Scaleway (language models like OpenAI OSS, Llama 4, Qwen 3, and more) - - Cartesia (voice model that supports both text-to-speech and speech-to-text) - * Add comprehensive message system with role-based messaging: - - `UserMessage` for user inputs with multi-modal content - - `SystemMessage` for system instructions - - `AssistantMessage` for AI responses - - `ToolCallMessage` for tool execution results - * Add support for multiple content types: - - Text, Image, ImageUrl, Audio, Document, DocumentUrl, File - * Add capability system for runtime model feature detection: - - Input capabilities: TEXT, MESSAGES, IMAGE, AUDIO, PDF, MULTIPLE - - Output capabilities: TEXT, IMAGE, AUDIO, STREAMING, STRUCTURED - - Advanced capabilities: TOOL_CALLING - * Add multiple response types: - - `TextResponse` for standard text responses - - `VectorResponse` for embedding vectors - - `BinaryResponse` for binary data (images, audio) - - `StreamResponse` for Server-Sent Events streaming - - `ChoiceResponse` for multiple choice responses - - `ToolCallResponse` for tool execution requests - - `ObjectResponse` for structured object responses - - `RawHttpResponse` for raw HTTP response access - * Add real-time response streaming via Server-Sent Events - * Add parallel processing support for concurrent model requests - * Add tool calling support with JSON Schema parameter validation - * Add contract system with normalizers for cross-platform compatibility - * Add HuggingFace task support: - - Text Classification, Token Classification, Fill Mask - - Question Answering, Table Question Answering - - Sentence Similarity, Zero-Shot Classification - - Object Detection, Image Segmentation - * Add metadata support for responses - * Add token usage tracking - * Add temperature and parameter controls - * Add exception handling with specific error types - * Add support for embeddings generation across multiple providers - * Add response promises for async operations - * Add InMemoryPlatform and InMemoryRawResult for testing Platform without external Providers calls - * Add tool calling support for Ollama platform - * Allow beta feature flags to be passed into Anthropic model options - * Add Ollama streaming output support - * Add multimodal embedding support for Voyage AI + * Add the component diff --git a/src/store/CHANGELOG.md b/src/store/CHANGELOG.md index fa54f16f1..86009a85a 100644 --- a/src/store/CHANGELOG.md +++ b/src/store/CHANGELOG.md @@ -4,64 +4,4 @@ CHANGELOG 0.1 --- - * Add core store interfaces: - - `StoreInterface` for storing and querying document stores - - `ManagedStoreInterface` for stores, which can be initialized or dropped - * Add document types: - - `TextDocument` for raw text with UUID, content, and metadata - - `VectorDocument` for vectorized documents with embeddings and similarity scores - - `Metadata` for key-value document metadata storage - * Add document loading system: - - `LoaderInterface` contract for various document sources - - `TextFileLoader` for loading text files as TextDocuments - * Add document transformation pipeline: - - `TransformerInterface` contract for document processing - - `TextSplitTransformer` for splitting large documents into chunks - - `ChainTransformer` for combining multiple transformers - - `ChunkDelayTransformer` for rate limiting during processing - * Add vectorization support: - - `Vectorizer` for converting TextDocuments to VectorDocuments - - Batch vectorization support for compatible platforms - - Single document vectorization with fallback - * Add high-level `Indexer` service: - - Orchestrates document processing pipeline - - Accepts TextDocuments, vectorizes and stores in chunks - - Configurable batch processing - * Add `InMemoryStore` and `CacheStore` implementations with multiple distance algorithms: - - Cosine similarity - - Angular distance - - Euclidean distance - - Manhattan distance - - Chebyshev distance - * Add store bridge implementations: - - Azure AI Search - - ChromaDB - - ClickHouse - - Cloudflare - - Elasticsearch - - Manticore Search - - MariaDB - - Meilisearch - - MongoDB - - Neo4j - - OpenSearch - - Pinecone - - PostgreSQL with pgvector extension - - Qdrant - - Redis - - Supabase - - SurrealDB - - Typesense - - Weaviate - * Add Retrieval Augmented Generation (RAG) support: - - Document embedding storage - - Similarity search for relevant documents - - Dynamic context extension for AI applications - * Add query features: - - Vector similarity search with configurable options - - Minimum score filtering - - Result limiting - - Distance/similarity scoring - * Add Meilisearch hybrid search support with a configurable `semanticRatio` parameter to control the balance between semantic (vector) and full-text search. - * Add custom exception hierarchy with `ExceptionInterface` - * Add support for specific exceptions for invalid arguments and runtime errors + * Add the component diff --git a/src/store/src/Bridge/OpenSearch/CHANGELOG.md b/src/store/src/Bridge/OpenSearch/CHANGELOG.md index c1d19f83e..0915f3546 100644 --- a/src/store/src/Bridge/OpenSearch/CHANGELOG.md +++ b/src/store/src/Bridge/OpenSearch/CHANGELOG.md @@ -4,4 +4,4 @@ CHANGELOG 0.1 --- -* Add the store + * Add the bridge From 0ea44682b0defee5a36572568874c4174454101c Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Fri, 19 Dec 2025 15:21:36 +0100 Subject: [PATCH 13/17] Minor patches to repair some examples --- examples/aimlapi/image-input-binary.php | 2 +- examples/bootstrap.php | 5 +++- examples/indexer/index-with-filters.php | 6 +++-- examples/mistral/chat-multiple.php | 4 +-- examples/mistral/token-metadata.php | 2 +- .../src/Bridge/Ollama/ModelCatalog.php | 15 +++++------ .../Bridge/Ollama/Tests/ModelCatalogTest.php | 8 +++--- .../Ollama/Tests/TokenUsageExtractorTest.php | 26 ++++++------------- .../src/Bridge/Ollama/TokenUsageExtractor.php | 14 +++++----- 9 files changed, 36 insertions(+), 46 deletions(-) diff --git a/examples/aimlapi/image-input-binary.php b/examples/aimlapi/image-input-binary.php index 646c647e3..3a76f3c37 100644 --- a/examples/aimlapi/image-input-binary.php +++ b/examples/aimlapi/image-input-binary.php @@ -25,6 +25,6 @@ Image::fromFile(dirname(__DIR__, 2).'/fixtures/image.jpg'), ), ); -$result = $platform->invoke('google/gemma-3-27b-it', $messages); +$result = $platform->invoke('google/gemini-2.5-pro', $messages); echo $result->asText().\PHP_EOL; diff --git a/examples/bootstrap.php b/examples/bootstrap.php index 03e7b6372..3dcefaf56 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -101,7 +101,10 @@ function print_token_usage(Metadata $metadata): void { $tokenUsage = $metadata->get('token_usage'); - assert($tokenUsage instanceof TokenUsage); + if (!$tokenUsage instanceof TokenUsage) { + output()->writeln('No token usage information available.'); + exit(1); + } $na = 'n/a'; $table = new Table(output()); diff --git a/examples/indexer/index-with-filters.php b/examples/indexer/index-with-filters.php index 4f7894375..916c094ad 100644 --- a/examples/indexer/index-with-filters.php +++ b/examples/indexer/index-with-filters.php @@ -72,16 +72,18 @@ $vector = $vectorizer->vectorize('technology artificial intelligence'); $results = $store->query($vector); +$filteredDocuments = 0; foreach ($results as $i => $document) { $title = $document->metadata['title'] ?? 'Unknown'; $category = $document->metadata['category'] ?? 'Unknown'; echo sprintf("%d. %s [%s]\n", $i + 1, $title, $category); echo sprintf(" Content: %s\n", substr($document->metadata->getText() ?? 'No content', 0, 80).'...'); echo sprintf(" ID: %s\n\n", substr($document->id, 0, 8).'...'); + ++$filteredDocuments; } echo "=== Results Summary ===\n"; echo sprintf("Original documents: %d\n", count($documents)); -echo sprintf("Documents after filtering: %d\n", count($results)); -echo sprintf("Filtered out: %d documents\n", count($documents) - count($results)); +echo sprintf("Documents after filtering: %d\n", ++$filteredDocuments); +echo sprintf("Filtered out: %d documents\n", count($documents) - $filteredDocuments); echo "\nThe 'Week of Symfony' newsletter and SPAM advertisement were successfully filtered out!\n"; diff --git a/examples/mistral/chat-multiple.php b/examples/mistral/chat-multiple.php index ae731fc6e..80478fd5f 100644 --- a/examples/mistral/chat-multiple.php +++ b/examples/mistral/chat-multiple.php @@ -21,9 +21,9 @@ Message::forSystem('Just give short answers.'), Message::ofUser('What is your favorite color?'), ); -$result = $platform->invoke('mistral-large-latest', $messages, [ +$result = $platform->invoke('mistral-small-latest', $messages, [ 'temperature' => 1.5, - 'n' => 10, + 'n' => 5, ]); foreach ($result->getResult()->getContent() as $key => $choice) { diff --git a/examples/mistral/token-metadata.php b/examples/mistral/token-metadata.php index ab8c3ee78..513e702b1 100644 --- a/examples/mistral/token-metadata.php +++ b/examples/mistral/token-metadata.php @@ -21,7 +21,7 @@ $agent = new Agent($platform, 'mistral-large-latest'); $messages = new MessageBag( - Message::forSystem('You are a pirate and you write funny.'), + Message::forSystem('You are a pirate, you write funny and only with one sentence.'), Message::ofUser('What is the best French cuisine?'), ); diff --git a/src/platform/src/Bridge/Ollama/ModelCatalog.php b/src/platform/src/Bridge/Ollama/ModelCatalog.php index 55bb7bd8c..80c0bb22d 100644 --- a/src/platform/src/Bridge/Ollama/ModelCatalog.php +++ b/src/platform/src/Bridge/Ollama/ModelCatalog.php @@ -192,28 +192,25 @@ public function __construct(array $additionalModels = []) 'nomic-embed-text' => [ 'class' => Ollama::class, 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, + Capability::INPUT_TEXT, Capability::INPUT_MULTIPLE, + Capability::EMBEDDINGS, ], ], 'bge-m3' => [ 'class' => Ollama::class, 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, + Capability::INPUT_TEXT, Capability::INPUT_MULTIPLE, + Capability::EMBEDDINGS, ], ], 'all-minilm' => [ 'class' => Ollama::class, 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, + Capability::INPUT_TEXT, Capability::INPUT_MULTIPLE, + Capability::EMBEDDINGS, ], ], ]; diff --git a/src/platform/src/Bridge/Ollama/Tests/ModelCatalogTest.php b/src/platform/src/Bridge/Ollama/Tests/ModelCatalogTest.php index e296eb7ce..58d6cb7bf 100644 --- a/src/platform/src/Bridge/Ollama/Tests/ModelCatalogTest.php +++ b/src/platform/src/Bridge/Ollama/Tests/ModelCatalogTest.php @@ -44,10 +44,10 @@ public static function modelsProvider(): iterable yield 'gemma2' => ['gemma2', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; yield 'gemma' => ['gemma', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; yield 'llama2' => ['llama2', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'nomic-embed-text' => ['nomic-embed-text', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::INPUT_MULTIPLE]]; - yield 'bge-m3' => ['bge-m3', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::INPUT_MULTIPLE]]; - yield 'all-minilm' => ['all-minilm', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::INPUT_MULTIPLE]]; - yield 'all-minilm:33m' => ['all-minilm:33m', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::INPUT_MULTIPLE]]; + yield 'nomic-embed-text' => ['nomic-embed-text', Ollama::class, [Capability::INPUT_TEXT, Capability::EMBEDDINGS, Capability::INPUT_MULTIPLE]]; + yield 'bge-m3' => ['bge-m3', Ollama::class, [Capability::INPUT_TEXT, Capability::EMBEDDINGS, Capability::INPUT_MULTIPLE]]; + yield 'all-minilm' => ['all-minilm', Ollama::class, [Capability::INPUT_TEXT, Capability::EMBEDDINGS, Capability::INPUT_MULTIPLE]]; + yield 'all-minilm:33m' => ['all-minilm:33m', Ollama::class, [Capability::INPUT_TEXT, Capability::EMBEDDINGS, Capability::INPUT_MULTIPLE]]; } protected function createModelCatalog(): ModelCatalogInterface diff --git a/src/platform/src/Bridge/Ollama/Tests/TokenUsageExtractorTest.php b/src/platform/src/Bridge/Ollama/Tests/TokenUsageExtractorTest.php index ed07f67ee..ce76283a0 100644 --- a/src/platform/src/Bridge/Ollama/Tests/TokenUsageExtractorTest.php +++ b/src/platform/src/Bridge/Ollama/Tests/TokenUsageExtractorTest.php @@ -11,12 +11,10 @@ namespace Symfony\AI\Platform\Bridge\Ollama\Tests; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\AI\Platform\Bridge\Ollama\TokenUsageExtractor; use Symfony\AI\Platform\Result\InMemoryRawResult; use Symfony\AI\Platform\TokenUsage\TokenUsage; -use Symfony\Contracts\HttpClient\ResponseInterface; final class TokenUsageExtractorTest extends TestCase { @@ -37,7 +35,13 @@ public function testItDoesNothingWithoutUsageData() public function testItExtractsTokenUsage() { $extractor = new TokenUsageExtractor(); - $result = new InMemoryRawResult([], object: $this->createResponseObject()); + $result = new InMemoryRawResult([ + 'model' => 'foo', + 'response' => 'Hello World!', + 'done' => true, + 'prompt_eval_count' => 10, + 'eval_count' => 10, + ]); $tokenUsage = $extractor->extract($result); @@ -63,7 +67,7 @@ public function testItExtractsTokenUsageFromStreamResult() 'prompt_eval_count' => 10, 'eval_count' => 10, ], - ], object: $this->createResponseObject()); + ]); $tokenUsage = $extractor->extract($result, ['stream' => true]); @@ -71,18 +75,4 @@ public function testItExtractsTokenUsageFromStreamResult() $this->assertSame(10, $tokenUsage->getPromptTokens()); $this->assertSame(10, $tokenUsage->getCompletionTokens()); } - - private function createResponseObject(): ResponseInterface|MockObject - { - $response = $this->createStub(ResponseInterface::class); - $response->method('toArray')->willReturn([ - 'model' => 'foo', - 'response' => 'Hello World!', - 'done' => true, - 'prompt_eval_count' => 10, - 'eval_count' => 10, - ]); - - return $response; - } } diff --git a/src/platform/src/Bridge/Ollama/TokenUsageExtractor.php b/src/platform/src/Bridge/Ollama/TokenUsageExtractor.php index bfeb3f7f0..57d869653 100644 --- a/src/platform/src/Bridge/Ollama/TokenUsageExtractor.php +++ b/src/platform/src/Bridge/Ollama/TokenUsageExtractor.php @@ -15,7 +15,6 @@ use Symfony\AI\Platform\TokenUsage\TokenUsage; use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\AI\Platform\TokenUsage\TokenUsageInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Guillaume Loulier @@ -24,11 +23,6 @@ final class TokenUsageExtractor implements TokenUsageExtractorInterface { public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsageInterface { - $response = $rawResult->getObject(); - if (!$response instanceof ResponseInterface) { - return null; - } - if ($options['stream'] ?? false) { foreach ($rawResult->getDataStream() as $chunk) { if ($chunk['done']) { @@ -42,11 +36,15 @@ public function extract(RawResultInterface $rawResult, array $options = []): ?To return null; } - $payload = $response->toArray(); + $payload = $rawResult->getData(); + + if (!isset($payload['prompt_eval_count'], $payload['eval_count'])) { + return null; + } return new TokenUsage( $payload['prompt_eval_count'], - $payload['eval_count'] + $payload['eval_count'], ); } } From fee5d573e42801b9049c1f0575009fbe9e6b2e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 19 Dec 2025 16:19:11 +0100 Subject: [PATCH 14/17] [Platform][OpenAi] Add support for gpt-5.2 --- src/platform/src/Bridge/OpenAi/ModelCatalog.php | 12 ++++++++++++ .../src/ModelCatalog/AbstractModelCatalog.php | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/platform/src/Bridge/OpenAi/ModelCatalog.php b/src/platform/src/Bridge/OpenAi/ModelCatalog.php index e1f9bdf44..a5afe4fe8 100644 --- a/src/platform/src/Bridge/OpenAi/ModelCatalog.php +++ b/src/platform/src/Bridge/OpenAi/ModelCatalog.php @@ -228,6 +228,18 @@ public function __construct(array $additionalModels = []) Capability::OUTPUT_STRUCTURED, ], ], + 'gpt-5.2' => [ + 'class' => Gpt::class, + 'capabilities' => [ + Capability::INPUT_IMAGE, + Capability::INPUT_MESSAGES, + Capability::INPUT_PDF, + Capability::OUTPUT_STREAMING, + Capability::OUTPUT_STRUCTURED, + Capability::OUTPUT_TEXT, + Capability::TOOL_CALLING, + ], + ], 'text-embedding-ada-002' => [ 'class' => Embeddings::class, 'capabilities' => [Capability::INPUT_TEXT], diff --git a/src/platform/src/ModelCatalog/AbstractModelCatalog.php b/src/platform/src/ModelCatalog/AbstractModelCatalog.php index 40b4f6238..e0d2fd5f4 100644 --- a/src/platform/src/ModelCatalog/AbstractModelCatalog.php +++ b/src/platform/src/ModelCatalog/AbstractModelCatalog.php @@ -38,7 +38,7 @@ public function getModel(string $modelName): Model $options = $parsed['options']; if (!isset($this->models[$catalogKey])) { - throw new ModelNotFoundException(\sprintf('Model "%s" not found.', $actualModelName)); + throw new ModelNotFoundException(\sprintf('Model "%s" not found in %s.', $actualModelName, static::class)); } $modelConfig = $this->models[$catalogKey]; From 587cf99cdf2c09bc6483d10bc95c49db4844a657 Mon Sep 17 00:00:00 2001 From: camilleislasse Date: Wed, 17 Dec 2025 12:53:18 +0100 Subject: [PATCH 15/17] [AI Bundle] Fix profiler serialization error with unconsumed streams --- src/ai-bundle/src/Profiler/DataCollector.php | 3 ++- .../tests/Profiler/DataCollectorTest.php | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/ai-bundle/src/Profiler/DataCollector.php b/src/ai-bundle/src/Profiler/DataCollector.php index 51bf91c05..23feba48c 100644 --- a/src/ai-bundle/src/Profiler/DataCollector.php +++ b/src/ai-bundle/src/Profiler/DataCollector.php @@ -155,7 +155,8 @@ private function awaitCallResults(TraceablePlatform $platform): array if (isset($platform->resultCache[$result])) { $call['result'] = $platform->resultCache[$result]; } else { - $call['result'] = $result->getContent(); + $content = $result->getContent(); + $call['result'] = $content instanceof \Generator ? null : $content; } $call['metadata'] = $result->getMetadata(); diff --git a/src/ai-bundle/tests/Profiler/DataCollectorTest.php b/src/ai-bundle/tests/Profiler/DataCollectorTest.php index 0d1595df7..9f2da1367 100644 --- a/src/ai-bundle/tests/Profiler/DataCollectorTest.php +++ b/src/ai-bundle/tests/Profiler/DataCollectorTest.php @@ -76,6 +76,30 @@ public function testCollectsDataForStreamingResponse() $this->assertSame('Assistant response', $dataCollector->getPlatformCalls()[0]['result']); } + public function testCollectsDataForUnconsumedStreamingResponse() + { + $platform = $this->createMock(PlatformInterface::class); + $traceablePlatform = new TraceablePlatform($platform); + $messageBag = new MessageBag(Message::ofUser(new Text('Hello'))); + $result = new StreamResult( + (function () { + yield 'Assistant '; + yield 'response'; + })(), + ); + + $platform->method('invoke')->willReturn(new DeferredResult(new PlainConverter($result), $this->createStub(RawResultInterface::class))); + + // Invoke but do NOT consume the stream + $traceablePlatform->invoke('gpt-4o', $messageBag, ['stream' => true]); + + $dataCollector = new DataCollector([$traceablePlatform], [], [], []); + $dataCollector->lateCollect(); + + $this->assertCount(1, $dataCollector->getPlatformCalls()); + $this->assertNull($dataCollector->getPlatformCalls()[0]['result']); + } + public function testCollectsDataForMessageStore() { $traceableMessageStore = new TraceableMessageStore(new InMemoryStore(), new MonotonicClock()); From 4fbf15cd599da4617ed3a60184b3f57537c0c58b Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 19 Dec 2025 15:37:59 +0100 Subject: [PATCH 16/17] [Platform][DeepSeek] Add TokenUsageExtractor tests Add test coverage for the DeepSeek TokenUsageExtractor including tests for stream handling, missing usage data, full token extraction, and partial usage fields. --- .../Tests/TokenUsageExtractorTest.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/platform/src/Bridge/DeepSeek/Tests/TokenUsageExtractorTest.php diff --git a/src/platform/src/Bridge/DeepSeek/Tests/TokenUsageExtractorTest.php b/src/platform/src/Bridge/DeepSeek/Tests/TokenUsageExtractorTest.php new file mode 100644 index 000000000..93781516e --- /dev/null +++ b/src/platform/src/Bridge/DeepSeek/Tests/TokenUsageExtractorTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\DeepSeek; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\DeepSeek\TokenUsageExtractor; +use Symfony\AI\Platform\Result\InMemoryRawResult; +use Symfony\AI\Platform\TokenUsage\TokenUsage; + +/** + * @author Oskar Stark + */ +final class TokenUsageExtractorTest extends TestCase +{ + public function testItHandlesStreamResponsesWithoutProcessing() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(), ['stream' => true])); + } + + public function testItDoesNothingWithoutUsageData() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(['some' => 'data']))); + } + + public function testItExtractsTokenUsage() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + 'prompt_tokens' => 10, + 'completion_tokens' => 20, + 'total_tokens' => 30, + 'prompt_cache_hit_tokens' => 5, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertSame(5, $tokenUsage->getCachedTokens()); + $this->assertSame(30, $tokenUsage->getTotalTokens()); + } + + public function testItHandlesMissingUsageFields() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + 'prompt_tokens' => 10, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getCachedTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); + } +} From db564b7992af7fef53d8c017109345d9039cb268 Mon Sep 17 00:00:00 2001 From: Diego Aguiar Date: Fri, 19 Dec 2025 11:39:31 -0700 Subject: [PATCH 17/17] Use Symfony AI custom exceptions --- src/store/src/Bridge/Pinecone/Store.php | 7 ++++--- src/store/src/Bridge/Pinecone/Tests/StoreTest.php | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/store/src/Bridge/Pinecone/Store.php b/src/store/src/Bridge/Pinecone/Store.php index b063fefe1..507912f43 100644 --- a/src/store/src/Bridge/Pinecone/Store.php +++ b/src/store/src/Bridge/Pinecone/Store.php @@ -16,6 +16,7 @@ use Symfony\AI\Platform\Vector\Vector; use Symfony\AI\Store\Document\Metadata; use Symfony\AI\Store\Document\VectorDocument; +use Symfony\AI\Store\Exception\InvalidArgumentException; use Symfony\AI\Store\ManagedStoreInterface; use Symfony\AI\Store\StoreInterface; use Symfony\Component\Uid\Uuid; @@ -40,11 +41,11 @@ public function __construct( public function setup(array $options = []): void { if (null === $this->indexName) { - throw new \InvalidArgumentException('You need to configure and index name for the Pinecone store.'); + throw new InvalidArgumentException('You need to configure and index name for the Pinecone store.'); } if (false === isset($options['dimension'])) { - throw new \InvalidArgumentException('No supported options.'); + throw new InvalidArgumentException('No supported options.'); } $this->pinecone @@ -99,7 +100,7 @@ public function query(Vector $vector, array $options = []): iterable public function drop(array $options = []): void { if (null === $this->indexName) { - throw new \InvalidArgumentException('You need to configure and index name for the Pinecone store.'); + throw new InvalidArgumentException('You need to configure and index name for the Pinecone store.'); } $this->pinecone diff --git a/src/store/src/Bridge/Pinecone/Tests/StoreTest.php b/src/store/src/Bridge/Pinecone/Tests/StoreTest.php index a4bf78e49..480b55dbd 100644 --- a/src/store/src/Bridge/Pinecone/Tests/StoreTest.php +++ b/src/store/src/Bridge/Pinecone/Tests/StoreTest.php @@ -22,6 +22,7 @@ use Symfony\AI\Store\Bridge\Pinecone\Store; use Symfony\AI\Store\Document\Metadata; use Symfony\AI\Store\Document\VectorDocument; +use Symfony\AI\Store\Exception\InvalidArgumentException; use Symfony\Component\Uid\Uuid; final class StoreTest extends TestCase @@ -31,7 +32,7 @@ public function testStoreCantSetupWithInvalidOptions() $pinecone = $this->createMock(Client::class); $store = new Store($pinecone, 'text-index'); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $store->setup(); } @@ -41,7 +42,7 @@ public function testStoreCantSetupWithoutIndexName() $pinecone = $this->createMock(Client::class); $store = new Store($pinecone); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $store->setup(['dimension' => 1536]); } @@ -76,7 +77,7 @@ public function testStoreCantDropWithInvalidOptions() $pinecone = $this->createMock(Client::class); $store = new Store($pinecone); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $store->drop(); }