diff --git a/docs/components/chat.rst b/docs/components/chat.rst index 79e1205ef..cf1b046f9 100644 --- a/docs/components/chat.rst +++ b/docs/components/chat.rst @@ -37,6 +37,7 @@ You can find more advanced usage in combination with an Agent using the store fo * `Current session context storage with HttpFoundation session`_ * `Current process context storage with InMemory`_ * `Long-term context with Meilisearch`_ +* `Long-term context with Pogocache`_ Supported Message stores ------------------------ @@ -45,6 +46,7 @@ Supported Message stores * `HttpFoundation session`_ * `InMemory`_ * `Meilisearch`_ +* `Pogocache`_ Implementing a Bridge --------------------- @@ -124,7 +126,9 @@ store and ``bin/console ai:message-store:drop`` to clean up the message store: .. _`Current session context storage with HttpFoundation session`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-session.php .. _`Current process context storage with InMemory`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat.php .. _`Long-term context with Meilisearch`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-meilisearch.php +.. _`Long-term context with Pogocache`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-pogocache.php .. _`Cache`: https://symfony.com/doc/current/components/cache.html .. _`InMemory`: https://www.php.net/manual/en/language.types.array.php .. _`HttpFoundation session`: https://developers.cloudflare.com/vectorize/ .. _`Meilisearch`: https://www.meilisearch.com/ +.. _`Pogocache`: https://pogocache.com/ diff --git a/examples/.env b/examples/.env index 396c9540b..84bc3bd5b 100644 --- a/examples/.env +++ b/examples/.env @@ -164,3 +164,7 @@ SUPABASE_TABLE=documents SUPABASE_VECTOR_FIELD=embedding SUPABASE_VECTOR_DIMENSION=768 # when using Ollama with nomic-embed-text SUPABASE_MATCH_FUNCTION=match_documents + +# Pogocache (message store) +POGOCACHE_HOST=http://127.0.0.1:9401 +POGOCACHE_PASSWORD=symfony diff --git a/examples/chat/persistent-chat-pogocache.php b/examples/chat/persistent-chat-pogocache.php new file mode 100644 index 000000000..6a7ef8b0a --- /dev/null +++ b/examples/chat/persistent-chat-pogocache.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Agent\Agent; +use Symfony\AI\Chat\Bridge\Pogocache\MessageStore; +use Symfony\AI\Chat\Chat; +use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client()); + +$store = new MessageStore(http_client(), env('POGOCACHE_HOST'), env('POGOCACHE_PASSWORD')); +$store->setup(); + +$agent = new Agent($platform, 'gpt-4o-mini'); +$chat = new Chat($agent, $store); + +$messages = new MessageBag( + Message::forSystem('You are a helpful assistant. You only answer with short sentences.'), +); + +$chat->initiate($messages); +$chat->submit(Message::ofUser('My name is Christopher.')); +$message = $chat->submit(Message::ofUser('What is my name?')); + +echo $message->getContent().\PHP_EOL; diff --git a/examples/commands/message-stores.php b/examples/commands/message-stores.php index adbf5a45d..a5445704a 100644 --- a/examples/commands/message-stores.php +++ b/examples/commands/message-stores.php @@ -15,6 +15,7 @@ use Symfony\AI\Chat\Bridge\Local\CacheStore; use Symfony\AI\Chat\Bridge\Local\InMemoryStore; use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore as MeilisearchMessageStore; +use Symfony\AI\Chat\Bridge\Pogocache\MessageStore as PogocacheMessageStore; use Symfony\AI\Chat\Command\DropStoreCommand; use Symfony\AI\Chat\Command\SetupStoreCommand; use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -38,6 +39,12 @@ 'symfony', ), 'memory' => static fn (): InMemoryStore => new InMemoryStore('symfony'), + 'pogocache' => static fn (): PogocacheMessageStore => new PogocacheMessageStore( + http_client(), + env('POGOCACHE_HOST'), + env('POGOCACHE_PASSWORD'), + 'symfony', + ), 'session' => static function (): SessionStore { $request = Request::create('/'); $request->setSession(new Session(new MockArraySessionStorage())); diff --git a/examples/compose.yaml b/examples/compose.yaml index 54ed2000f..8738805f0 100644 --- a/examples/compose.yaml +++ b/examples/compose.yaml @@ -110,6 +110,12 @@ services: - '7474:7474' - '7687:7687' + pogocache: + image: pogocache/pogocache + command: [ 'pogocache', '--auth', 'symfony' ] + ports: + - '9401:9401' + postgres: image: pgvector/pgvector:0.8.0-pg17 environment: diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 6a9ee82af..17343a2ec 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -755,6 +755,16 @@ ->end() ->end() ->end() + ->arrayNode('pogocache') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->stringNode('endpoint')->cannotBeEmpty()->end() + ->stringNode('password')->cannotBeEmpty()->end() + ->stringNode('key')->cannotBeEmpty()->end() + ->end() + ->end() + ->end() ->arrayNode('session') ->useAttributeAsKey('name') ->arrayPrototype() diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index ce8e9a69c..23838bf1c 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -36,6 +36,7 @@ use Symfony\AI\AiBundle\Security\Attribute\IsGrantedTool; use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore; use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore as MeilisearchMessageStore; +use Symfony\AI\Chat\Bridge\Pogocache\MessageStore as PogocacheMessageStore; use Symfony\AI\Chat\MessageStoreInterface; use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory; use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory; @@ -1351,6 +1352,24 @@ private function processMessageStoreConfig(string $type, array $messageStores, C } } + if ('pogocache' === $type) { + foreach ($messageStores as $name => $messageStore) { + $definition = new Definition(PogocacheMessageStore::class); + $definition + ->setArguments([ + new Reference('http_client'), + $messageStore['endpoint'], + $messageStore['password'], + $messageStore['key'], + ]) + ->addTag('ai.message_store'); + + $container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition); + $container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name); + $container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name); + } + } + if ('session' === $type) { foreach ($messageStores as $name => $messageStore) { $definition = new Definition(SessionStore::class); diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 5887234b6..2fb189b8a 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -3042,6 +3042,13 @@ private function getFullConfig(): array 'index_name' => 'test', ], ], + 'pogocache' => [ + 'my_pogocache_message_store' => [ + 'endpoint' => 'http://127.0.0.1:9401', + 'password' => 'foo', + 'key' => 'bar', + ], + ], 'session' => [ 'my_session_message_store' => [ 'identifier' => 'session', diff --git a/src/chat/src/Bridge/Pogocache/MessageStore.php b/src/chat/src/Bridge/Pogocache/MessageStore.php new file mode 100644 index 000000000..4b95745d0 --- /dev/null +++ b/src/chat/src/Bridge/Pogocache/MessageStore.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Chat\Bridge\Pogocache; + +use Symfony\AI\Chat\Exception\InvalidArgumentException; +use Symfony\AI\Chat\Exception\LogicException; +use Symfony\AI\Chat\ManagedStoreInterface; +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\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Guillaume Loulier + */ +final readonly class MessageStore implements ManagedStoreInterface, MessageStoreInterface +{ + public function __construct( + private HttpClientInterface $httpClient, + private string $host, + #[\SensitiveParameter] private string $password, + private string $key = '_message_store_pogocache', + ) { + } + + public function setup(array $options = []): void + { + if ([] !== $options) { + throw new InvalidArgumentException('The Pogocache message store does not support any options.'); + } + + $this->request('PUT', $this->key); + } + + public function drop(): void + { + $this->request('PUT', $this->key); + } + + public function save(MessageBag $messages): void + { + $messages = $messages->getMessages(); + + $this->request('PUT', $this->key, array_map( + $this->convertToIndexableArray(...), + $messages, + )); + } + + public function load(): MessageBag + { + $messages = $this->request('GET', $this->key); + + return new MessageBag(...array_map( + $this->convertToMessage(...), + $messages, + )); + } + + /** + * @param array|list> $payload + * + * @return array + */ + private function request(string $method, string $endpoint, array $payload = []): array + { + $result = $this->httpClient->request($method, \sprintf('%s/%s?auth=%s', $this->host, $endpoint, $this->password), [ + 'json' => [] !== $payload ? $payload : new \stdClass(), + ]); + + $payload = $result->getContent(); + + if ('GET' === $method && json_validate($payload)) { + return json_decode($payload, true); + } + + return []; + } + + /** + * @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(), + ]; + } + + /** + * @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): ContentInterface => \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 ad6573df6..904120796 100644 --- a/src/chat/tests/Bridge/Meilisearch/MessageStoreTest.php +++ b/src/chat/tests/Bridge/Meilisearch/MessageStoreTest.php @@ -12,22 +12,16 @@ 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\Platform\Message\AssistantMessage; -use Symfony\AI\Platform\Message\Content\Text; +use Symfony\AI\Chat\Tests\MessageStoreTestCase; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; -use Symfony\AI\Platform\Message\SystemMessage; -use Symfony\AI\Platform\Message\ToolCallMessage; -use Symfony\AI\Platform\Message\UserMessage; 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\Uid\Uuid; -final class MessageStoreTest extends TestCase +final class MessageStoreTest extends MessageStoreTestCase { public function testStoreCannotSetupOnInvalidResponse() { @@ -244,69 +238,4 @@ public function testStoreCanLoadMessages(array $payload) $this->assertCount(1, $messageBag); $this->assertSame(1, $httpClient->getRequestsCount()); } - - 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' => [], - ], - ]; - } } diff --git a/src/chat/tests/Bridge/Pogocache/MessageStoreTest.php b/src/chat/tests/Bridge/Pogocache/MessageStoreTest.php new file mode 100644 index 000000000..35effe8b7 --- /dev/null +++ b/src/chat/tests/Bridge/Pogocache/MessageStoreTest.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Chat\Tests\Bridge\Pogocache; + +use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\AI\Chat\Bridge\Pogocache\MessageStore; +use Symfony\AI\Chat\Tests\MessageStoreTestCase; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\JsonMockResponse; + +final class MessageStoreTest extends MessageStoreTestCase +{ + public function testStoreCannotSetupOnInvalidResponse() + { + $httpClient = new MockHttpClient([ + new JsonMockResponse([], [ + 'http_code' => 400, + ]), + ], 'http://127.0.0.1:9401'); + + $store = new MessageStore($httpClient, 'http://127.0.0.1:9401', 'test', 'test'); + + self::expectException(ClientException::class); + self::expectExceptionMessage('HTTP 400 returned for "http://127.0.0.1:9401/test?auth=test".'); + self::expectExceptionCode(400); + $store->setup(); + } + + public function testStoreCanSetup() + { + $httpClient = new MockHttpClient([ + new JsonMockResponse('Stored', [ + 'http_code' => 200, + ]), + ], 'http://127.0.0.1:9401'); + + $store = new MessageStore($httpClient, 'http://127.0.0.1:9401', 'test', 'test'); + + $store->setup(); + + $this->assertSame(1, $httpClient->getRequestsCount()); + } + + public function testStoreCannotDropOnInvalidResponse() + { + $httpClient = new MockHttpClient([ + new JsonMockResponse('Not Found', [ + 'http_code' => 404, + ]), + ], 'http://127.0.0.1:9401'); + + $store = new MessageStore($httpClient, 'http://127.0.0.1:9401', 'test', 'test'); + + self::expectException(ClientException::class); + self::expectExceptionMessage('HTTP 404 returned for "http://127.0.0.1:9401/test?auth=test".'); + self::expectExceptionCode(404); + $store->drop(); + } + + public function testStoreCanDrop() + { + $httpClient = new MockHttpClient([ + new JsonMockResponse('Deleted', [ + 'http_code' => 200, + ]), + ], 'http://127.0.0.1:9401'); + + $store = new MessageStore($httpClient, 'http://127.0.0.1:9401', 'test', 'test'); + + $store->drop(); + + $this->assertSame(1, $httpClient->getRequestsCount()); + } + + public function testStoreCannotSaveOnInvalidResponse() + { + $httpClient = new MockHttpClient([ + new JsonMockResponse('', [ + 'http_code' => 400, + ]), + ], 'http://127.0.0.1:9401'); + + $store = new MessageStore($httpClient, 'http://127.0.0.1:9401', 'test', 'test'); + + self::expectException(ClientException::class); + self::expectExceptionMessage('HTTP 400 returned for "http://127.0.0.1:9401/test?auth=test".'); + self::expectExceptionCode(400); + $store->save(new MessageBag(Message::ofUser('Hello there'))); + } + + public function testStoreCanSave() + { + $httpClient = new MockHttpClient([ + new JsonMockResponse('Stored', [ + 'http_code' => 200, + ]), + ], 'http://127.0.0.1:9401'); + + $store = new MessageStore($httpClient, 'http://127.0.0.1:9401', 'test', 'test'); + + $store->save(new MessageBag(Message::ofUser('Hello there'))); + + $this->assertSame(1, $httpClient->getRequestsCount()); + } + + public function testStoreCannotLoadMessagesOnInvalidResponse() + { + $httpClient = new MockHttpClient([ + new JsonMockResponse('', [ + 'http_code' => 400, + ]), + ], 'http://127.0.0.1:9401'); + + $store = new MessageStore($httpClient, 'http://127.0.0.1:9401', 'test', 'test'); + + self::expectException(ClientException::class); + self::expectExceptionMessage('HTTP 400 returned for "http://127.0.0.1:9401/test?auth=test".'); + self::expectExceptionCode(400); + $store->load(); + } + + /** + * @param array $payload + */ + #[DataProvider('provideMessages')] + public function testStoreCanLoadMessages(array $payload) + { + $httpClient = new MockHttpClient([ + new JsonMockResponse([$payload], [ + 'http_code' => 200, + ]), + ], 'http://127.0.0.1:9401'); + + $store = new MessageStore($httpClient, 'http://127.0.0.1:9401', 'test', 'test'); + + $messageBag = $store->load(); + + $this->assertCount(1, $messageBag); + $this->assertSame(1, $httpClient->getRequestsCount()); + } +} diff --git a/src/chat/tests/MessageStoreTestCase.php b/src/chat/tests/MessageStoreTestCase.php new file mode 100644 index 000000000..0feb4a09b --- /dev/null +++ b/src/chat/tests/MessageStoreTestCase.php @@ -0,0 +1,88 @@ + + * + * 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' => [], + ], + ]; + } +}