From a3c77561ae486ca8bbd15b3697a619e31dd225e3 Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Wed, 19 Nov 2025 09:33:30 +0100 Subject: [PATCH] refactor(chat): Meilisearch store serializer usage --- src/ai-bundle/src/AiBundle.php | 1 + .../DependencyInjection/AiBundleTest.php | 3 + .../src/Bridge/Meilisearch/MessageStore.php | 115 +++--------------- .../Bridge/Meilisearch/MessageStoreTest.php | 27 ++-- src/chat/tests/MessageStoreTestCase.php | 88 -------------- 5 files changed, 37 insertions(+), 197 deletions(-) delete mode 100644 src/chat/tests/MessageStoreTestCase.php diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 36a8eff14..a8a4b42d2 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -1505,6 +1505,7 @@ private function processMessageStoreConfig(string $type, array $messageStores, C $messageStore['api_key'], new Reference(ClockInterface::class), $messageStore['index_name'], + new Reference('serializer'), ]) ->addTag('proxy', ['interface' => MessageStoreInterface::class]) ->addTag('ai.message_store'); diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 7174d9b4d..83c324617 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -2870,11 +2870,14 @@ public function testMeilisearchMessageStoreIsConfigured() $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')); diff --git a/src/chat/src/Bridge/Meilisearch/MessageStore.php b/src/chat/src/Bridge/Meilisearch/MessageStore.php index 911224784..83b7add7c 100644 --- a/src/chat/src/Bridge/Meilisearch/MessageStore.php +++ b/src/chat/src/Bridge/Meilisearch/MessageStore.php @@ -12,25 +12,19 @@ namespace Symfony\AI\Chat\Bridge\Meilisearch; use Symfony\AI\Chat\Exception\InvalidArgumentException; -use Symfony\AI\Chat\Exception\LogicException; use Symfony\AI\Chat\Exception\RuntimeException; use Symfony\AI\Chat\ManagedStoreInterface; +use Symfony\AI\Chat\MessageNormalizer; use Symfony\AI\Chat\MessageStoreInterface; -use Symfony\AI\Platform\Message\AssistantMessage; -use Symfony\AI\Platform\Message\Content\Audio; -use Symfony\AI\Platform\Message\Content\ContentInterface; -use Symfony\AI\Platform\Message\Content\DocumentUrl; -use Symfony\AI\Platform\Message\Content\File; -use Symfony\AI\Platform\Message\Content\Image; -use Symfony\AI\Platform\Message\Content\ImageUrl; -use Symfony\AI\Platform\Message\Content\Text; use Symfony\AI\Platform\Message\MessageBag; use Symfony\AI\Platform\Message\MessageInterface; -use Symfony\AI\Platform\Message\SystemMessage; -use Symfony\AI\Platform\Message\ToolCallMessage; -use Symfony\AI\Platform\Message\UserMessage; -use Symfony\AI\Platform\Result\ToolCall; use Symfony\Component\Clock\ClockInterface; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -45,6 +39,10 @@ public function __construct( #[\SensitiveParameter] private readonly string $apiKey, private readonly ClockInterface $clock, private readonly string $indexName = '_message_store_meilisearch', + private readonly SerializerInterface&NormalizerInterface&DenormalizerInterface $serializer = new Serializer([ + new ArrayDenormalizer(), + new MessageNormalizer(), + ], [new JsonEncoder()]), ) { if (!interface_exists(ClockInterface::class)) { throw new RuntimeException('For using Meilisearch as a message store , symfony/clock is required. Try running "composer require symfony/clock".'); @@ -74,7 +72,7 @@ public function save(MessageBag $messages): void $messages = $messages->getMessages(); $this->request('PUT', \sprintf('indexes/%s/documents', $this->indexName), array_map( - $this->convertToIndexableArray(...), + fn (MessageInterface $message): array => $this->serializer->normalize($message), $messages, )); } @@ -85,7 +83,10 @@ public function load(): MessageBag 'sort' => ['addedAt:asc'], ]); - return new MessageBag(...array_map($this->convertToMessage(...), $messages['results'])); + return new MessageBag(...array_map( + fn (array $message): MessageInterface => $this->serializer->denormalize($message, MessageInterface::class), + $messages['results'] + )); } public function drop(): void @@ -129,88 +130,4 @@ private function request(string $method, string $endpoint, array $payload = []): return $payload; } - - /** - * @return array - */ - private function convertToIndexableArray(MessageInterface $message): array - { - $toolsCalls = []; - - if ($message instanceof AssistantMessage && $message->hasToolCalls()) { - $toolsCalls = array_map( - static fn (ToolCall $toolCall): array => $toolCall->jsonSerialize(), - $message->getToolCalls(), - ); - } - - if ($message instanceof ToolCallMessage) { - $toolsCalls = $message->getToolCall()->jsonSerialize(); - } - - return [ - 'id' => $message->getId()->toRfc4122(), - 'type' => $message::class, - 'content' => ($message instanceof SystemMessage || $message instanceof AssistantMessage || $message instanceof ToolCallMessage) ? $message->getContent() : '', - 'contentAsBase64' => ($message instanceof UserMessage && [] !== $message->getContent()) ? array_map( - static fn (ContentInterface $content) => [ - 'type' => $content::class, - 'content' => match ($content::class) { - Text::class => $content->getText(), - File::class, - Image::class, - Audio::class => $content->asBase64(), - ImageUrl::class, - DocumentUrl::class => $content->getUrl(), - default => throw new LogicException(\sprintf('Unknown content type "%s".', $content::class)), - }, - ], - $message->getContent(), - ) : [], - 'toolsCalls' => $toolsCalls, - 'metadata' => $message->getMetadata()->all(), - 'addedAt' => (new \DateTimeImmutable())->getTimestamp(), - ]; - } - - /** - * @param array $payload - */ - private function convertToMessage(array $payload): MessageInterface - { - $type = $payload['type']; - $content = $payload['content'] ?? ''; - $contentAsBase64 = $payload['contentAsBase64'] ?? []; - - $message = match ($type) { - SystemMessage::class => new SystemMessage($content), - AssistantMessage::class => new AssistantMessage($content, array_map( - static fn (array $toolsCall): ToolCall => new ToolCall( - $toolsCall['id'], - $toolsCall['function']['name'], - json_decode($toolsCall['function']['arguments'], true) - ), - $payload['toolsCalls'], - )), - UserMessage::class => new UserMessage(...array_map( - static fn (array $contentAsBase64) => \in_array($contentAsBase64['type'], [File::class, Image::class, Audio::class], true) - ? $contentAsBase64['type']::fromDataUrl($contentAsBase64['content']) - : new $contentAsBase64['type']($contentAsBase64['content']), - $contentAsBase64, - )), - ToolCallMessage::class => new ToolCallMessage( - new ToolCall( - $payload['toolsCalls']['id'], - $payload['toolsCalls']['function']['name'], - json_decode($payload['toolsCalls']['function']['arguments'], true) - ), - $content - ), - default => throw new LogicException(\sprintf('Unknown message type "%s".', $type)), - }; - - $message->getMetadata()->set($payload['metadata']); - - return $message; - } } diff --git a/src/chat/tests/Bridge/Meilisearch/MessageStoreTest.php b/src/chat/tests/Bridge/Meilisearch/MessageStoreTest.php index 904120796..338c72ed5 100644 --- a/src/chat/tests/Bridge/Meilisearch/MessageStoreTest.php +++ b/src/chat/tests/Bridge/Meilisearch/MessageStoreTest.php @@ -11,17 +11,20 @@ namespace Symfony\AI\Chat\Tests\Bridge\Meilisearch; -use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore; -use Symfony\AI\Chat\Tests\MessageStoreTestCase; +use Symfony\AI\Chat\MessageNormalizer; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; use Symfony\Component\Clock\MonotonicClock; use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Serializer; -final class MessageStoreTest extends MessageStoreTestCase +final class MessageStoreTest extends TestCase { public function testStoreCannotSetupOnInvalidResponse() { @@ -215,27 +218,31 @@ public function testStoreCannotLoadMessagesOnInvalidResponse() $store->load(); } - /** - * @param array $payload - */ - #[DataProvider('provideMessages')] - public function testStoreCanLoadMessages(array $payload) + public function testStoreCanLoadMessages() { + $serializer = new Serializer([ + new ArrayDenormalizer(), + new MessageNormalizer(), + ], [new JsonEncoder()]); + $httpClient = new MockHttpClient([ new JsonMockResponse([ 'results' => [ - $payload, + $serializer->normalize(Message::ofUser('Hello World')), ], ], [ 'http_code' => 200, ]), ], 'http://127.0.0.1:7700'); - $store = new MessageStore($httpClient, 'http://127.0.0.1:7700', 'test', new MonotonicClock(), 'test'); + $store = new MessageStore($httpClient, 'http://127.0.0.1:7700', 'test', new MonotonicClock(), 'test', $serializer); $messageBag = $store->load(); $this->assertCount(1, $messageBag); $this->assertSame(1, $httpClient->getRequestsCount()); + + $storedMessage = $messageBag->getUserMessage(); + $this->assertSame('Hello World', $storedMessage->asText()); } } diff --git a/src/chat/tests/MessageStoreTestCase.php b/src/chat/tests/MessageStoreTestCase.php deleted file mode 100644 index 0feb4a09b..000000000 --- a/src/chat/tests/MessageStoreTestCase.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Chat\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\AI\Platform\Message\AssistantMessage; -use Symfony\AI\Platform\Message\Content\Text; -use Symfony\AI\Platform\Message\SystemMessage; -use Symfony\AI\Platform\Message\ToolCallMessage; -use Symfony\AI\Platform\Message\UserMessage; -use Symfony\Component\Uid\Uuid; - -class MessageStoreTestCase extends TestCase -{ - public static function provideMessages(): \Generator - { - yield UserMessage::class => [ - [ - 'id' => Uuid::v7()->toRfc4122(), - 'type' => UserMessage::class, - 'content' => '', - 'contentAsBase64' => [ - [ - 'type' => Text::class, - 'content' => 'What is the Symfony framework?', - ], - ], - 'toolsCalls' => [], - 'metadata' => [], - ], - ]; - yield SystemMessage::class => [ - [ - 'id' => Uuid::v7()->toRfc4122(), - 'type' => SystemMessage::class, - 'content' => 'Hello there', - 'contentAsBase64' => [], - 'toolsCalls' => [], - 'metadata' => [], - ], - ]; - yield AssistantMessage::class => [ - [ - 'id' => Uuid::v7()->toRfc4122(), - 'type' => AssistantMessage::class, - 'content' => 'Hello there', - 'contentAsBase64' => [], - 'toolsCalls' => [ - [ - 'id' => '1', - 'name' => 'foo', - 'function' => [ - 'name' => 'foo', - 'arguments' => '{}', - ], - ], - ], - 'metadata' => [], - ], - ]; - yield ToolCallMessage::class => [ - [ - 'id' => Uuid::v7()->toRfc4122(), - 'type' => ToolCallMessage::class, - 'content' => 'Hello there', - 'contentAsBase64' => [], - 'toolsCalls' => [ - 'id' => '1', - 'name' => 'foo', - 'function' => [ - 'name' => 'foo', - 'arguments' => '{}', - ], - ], - 'metadata' => [], - ], - ]; - } -}