Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/components/chat.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------------
Expand All @@ -45,6 +46,7 @@ Supported Message stores
* `HttpFoundation session`_
* `InMemory`_
* `Meilisearch`_
* `Pogocache`_

Implementing a Bridge
---------------------
Expand Down Expand Up @@ -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/
4 changes: 4 additions & 0 deletions examples/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 37 additions & 0 deletions examples/chat/persistent-chat-pogocache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
7 changes: 7 additions & 0 deletions examples/commands/message-stores.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()));
Expand Down
6 changes: 6 additions & 0 deletions examples/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
19 changes: 19 additions & 0 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
183 changes: 183 additions & 0 deletions src/chat/src/Bridge/Pogocache/MessageStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <personal@guillaumeloulier.fr>
*/
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<string, mixed>|list<array<string, mixed>> $payload
*
* @return array<string, mixed>
*/
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<string, mixed>
*/
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<string, mixed> $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;
}
}
Loading