Skip to content

Commit ff0bc3e

Browse files
committed
feature #788 [Chat] Add Pogocache as message store (Guikingone)
This PR was squashed before being merged into the main branch. Discussion ---------- [Chat] Add Pogocache as message store | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | yes | Issues | None | License | MIT Hi 👋🏻 This PR aims to bring the support for `Pogocache` as a message store (faster than Redis on benchmarks) via the HTTP API. Commits ------- a99232a [Chat] Add Pogocache as message store
2 parents 07603cf + a99232a commit ff0bc3e

File tree

12 files changed

+520
-73
lines changed

12 files changed

+520
-73
lines changed

docs/components/chat.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ You can find more advanced usage in combination with an Agent using the store fo
3737
* `Current session context storage with HttpFoundation session`_
3838
* `Current process context storage with InMemory`_
3939
* `Long-term context with Meilisearch`_
40+
* `Long-term context with Pogocache`_
4041

4142
Supported Message stores
4243
------------------------
@@ -45,6 +46,7 @@ Supported Message stores
4546
* `HttpFoundation session`_
4647
* `InMemory`_
4748
* `Meilisearch`_
49+
* `Pogocache`_
4850

4951
Implementing a Bridge
5052
---------------------
@@ -124,7 +126,9 @@ store and ``bin/console ai:message-store:drop`` to clean up the message store:
124126
.. _`Current session context storage with HttpFoundation session`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-session.php
125127
.. _`Current process context storage with InMemory`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat.php
126128
.. _`Long-term context with Meilisearch`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-meilisearch.php
129+
.. _`Long-term context with Pogocache`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-pogocache.php
127130
.. _`Cache`: https://symfony.com/doc/current/components/cache.html
128131
.. _`InMemory`: https://www.php.net/manual/en/language.types.array.php
129132
.. _`HttpFoundation session`: https://developers.cloudflare.com/vectorize/
130133
.. _`Meilisearch`: https://www.meilisearch.com/
134+
.. _`Pogocache`: https://pogocache.com/

examples/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,7 @@ SUPABASE_TABLE=documents
164164
SUPABASE_VECTOR_FIELD=embedding
165165
SUPABASE_VECTOR_DIMENSION=768 # when using Ollama with nomic-embed-text
166166
SUPABASE_MATCH_FUNCTION=match_documents
167+
168+
# Pogocache (message store)
169+
POGOCACHE_HOST=http://127.0.0.1:9401
170+
POGOCACHE_PASSWORD=symfony
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Chat\Bridge\Pogocache\MessageStore;
14+
use Symfony\AI\Chat\Chat;
15+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
16+
use Symfony\AI\Platform\Message\Message;
17+
use Symfony\AI\Platform\Message\MessageBag;
18+
19+
require_once dirname(__DIR__).'/bootstrap.php';
20+
21+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
22+
23+
$store = new MessageStore(http_client(), env('POGOCACHE_HOST'), env('POGOCACHE_PASSWORD'));
24+
$store->setup();
25+
26+
$agent = new Agent($platform, 'gpt-4o-mini');
27+
$chat = new Chat($agent, $store);
28+
29+
$messages = new MessageBag(
30+
Message::forSystem('You are a helpful assistant. You only answer with short sentences.'),
31+
);
32+
33+
$chat->initiate($messages);
34+
$chat->submit(Message::ofUser('My name is Christopher.'));
35+
$message = $chat->submit(Message::ofUser('What is my name?'));
36+
37+
echo $message->getContent().\PHP_EOL;

examples/commands/message-stores.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\AI\Chat\Bridge\Local\CacheStore;
1616
use Symfony\AI\Chat\Bridge\Local\InMemoryStore;
1717
use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore as MeilisearchMessageStore;
18+
use Symfony\AI\Chat\Bridge\Pogocache\MessageStore as PogocacheMessageStore;
1819
use Symfony\AI\Chat\Command\DropStoreCommand;
1920
use Symfony\AI\Chat\Command\SetupStoreCommand;
2021
use Symfony\Component\Cache\Adapter\ArrayAdapter;
@@ -38,6 +39,12 @@
3839
'symfony',
3940
),
4041
'memory' => static fn (): InMemoryStore => new InMemoryStore('symfony'),
42+
'pogocache' => static fn (): PogocacheMessageStore => new PogocacheMessageStore(
43+
http_client(),
44+
env('POGOCACHE_HOST'),
45+
env('POGOCACHE_PASSWORD'),
46+
'symfony',
47+
),
4148
'session' => static function (): SessionStore {
4249
$request = Request::create('/');
4350
$request->setSession(new Session(new MockArraySessionStorage()));

examples/compose.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ services:
110110
- '7474:7474'
111111
- '7687:7687'
112112

113+
pogocache:
114+
image: pogocache/pogocache
115+
command: [ 'pogocache', '--auth', 'symfony' ]
116+
ports:
117+
- '9401:9401'
118+
113119
postgres:
114120
image: pgvector/pgvector:0.8.0-pg17
115121
environment:

src/ai-bundle/config/options.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,16 @@
755755
->end()
756756
->end()
757757
->end()
758+
->arrayNode('pogocache')
759+
->useAttributeAsKey('name')
760+
->arrayPrototype()
761+
->children()
762+
->stringNode('endpoint')->cannotBeEmpty()->end()
763+
->stringNode('password')->cannotBeEmpty()->end()
764+
->stringNode('key')->cannotBeEmpty()->end()
765+
->end()
766+
->end()
767+
->end()
758768
->arrayNode('session')
759769
->useAttributeAsKey('name')
760770
->arrayPrototype()

src/ai-bundle/src/AiBundle.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Symfony\AI\AiBundle\Security\Attribute\IsGrantedTool;
3737
use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore;
3838
use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore as MeilisearchMessageStore;
39+
use Symfony\AI\Chat\Bridge\Pogocache\MessageStore as PogocacheMessageStore;
3940
use Symfony\AI\Chat\MessageStoreInterface;
4041
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
4142
use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory;
@@ -1351,6 +1352,24 @@ private function processMessageStoreConfig(string $type, array $messageStores, C
13511352
}
13521353
}
13531354

1355+
if ('pogocache' === $type) {
1356+
foreach ($messageStores as $name => $messageStore) {
1357+
$definition = new Definition(PogocacheMessageStore::class);
1358+
$definition
1359+
->setArguments([
1360+
new Reference('http_client'),
1361+
$messageStore['endpoint'],
1362+
$messageStore['password'],
1363+
$messageStore['key'],
1364+
])
1365+
->addTag('ai.message_store');
1366+
1367+
$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
1368+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
1369+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
1370+
}
1371+
}
1372+
13541373
if ('session' === $type) {
13551374
foreach ($messageStores as $name => $messageStore) {
13561375
$definition = new Definition(SessionStore::class);

src/ai-bundle/tests/DependencyInjection/AiBundleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3042,6 +3042,13 @@ private function getFullConfig(): array
30423042
'index_name' => 'test',
30433043
],
30443044
],
3045+
'pogocache' => [
3046+
'my_pogocache_message_store' => [
3047+
'endpoint' => 'http://127.0.0.1:9401',
3048+
'password' => 'foo',
3049+
'key' => 'bar',
3050+
],
3051+
],
30453052
'session' => [
30463053
'my_session_message_store' => [
30473054
'identifier' => 'session',
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Chat\Bridge\Pogocache;
13+
14+
use Symfony\AI\Chat\Exception\InvalidArgumentException;
15+
use Symfony\AI\Chat\Exception\LogicException;
16+
use Symfony\AI\Chat\ManagedStoreInterface;
17+
use Symfony\AI\Chat\MessageStoreInterface;
18+
use Symfony\AI\Platform\Message\AssistantMessage;
19+
use Symfony\AI\Platform\Message\Content\Audio;
20+
use Symfony\AI\Platform\Message\Content\ContentInterface;
21+
use Symfony\AI\Platform\Message\Content\DocumentUrl;
22+
use Symfony\AI\Platform\Message\Content\File;
23+
use Symfony\AI\Platform\Message\Content\Image;
24+
use Symfony\AI\Platform\Message\Content\ImageUrl;
25+
use Symfony\AI\Platform\Message\Content\Text;
26+
use Symfony\AI\Platform\Message\MessageBag;
27+
use Symfony\AI\Platform\Message\MessageInterface;
28+
use Symfony\AI\Platform\Message\SystemMessage;
29+
use Symfony\AI\Platform\Message\ToolCallMessage;
30+
use Symfony\AI\Platform\Message\UserMessage;
31+
use Symfony\AI\Platform\Result\ToolCall;
32+
use Symfony\Contracts\HttpClient\HttpClientInterface;
33+
34+
/**
35+
* @author Guillaume Loulier <[email protected]>
36+
*/
37+
final readonly class MessageStore implements ManagedStoreInterface, MessageStoreInterface
38+
{
39+
public function __construct(
40+
private HttpClientInterface $httpClient,
41+
private string $host,
42+
#[\SensitiveParameter] private string $password,
43+
private string $key = '_message_store_pogocache',
44+
) {
45+
}
46+
47+
public function setup(array $options = []): void
48+
{
49+
if ([] !== $options) {
50+
throw new InvalidArgumentException('The Pogocache message store does not support any options.');
51+
}
52+
53+
$this->request('PUT', $this->key);
54+
}
55+
56+
public function drop(): void
57+
{
58+
$this->request('PUT', $this->key);
59+
}
60+
61+
public function save(MessageBag $messages): void
62+
{
63+
$messages = $messages->getMessages();
64+
65+
$this->request('PUT', $this->key, array_map(
66+
$this->convertToIndexableArray(...),
67+
$messages,
68+
));
69+
}
70+
71+
public function load(): MessageBag
72+
{
73+
$messages = $this->request('GET', $this->key);
74+
75+
return new MessageBag(...array_map(
76+
$this->convertToMessage(...),
77+
$messages,
78+
));
79+
}
80+
81+
/**
82+
* @param array<string, mixed>|list<array<string, mixed>> $payload
83+
*
84+
* @return array<string, mixed>
85+
*/
86+
private function request(string $method, string $endpoint, array $payload = []): array
87+
{
88+
$result = $this->httpClient->request($method, \sprintf('%s/%s?auth=%s', $this->host, $endpoint, $this->password), [
89+
'json' => [] !== $payload ? $payload : new \stdClass(),
90+
]);
91+
92+
$payload = $result->getContent();
93+
94+
if ('GET' === $method && json_validate($payload)) {
95+
return json_decode($payload, true);
96+
}
97+
98+
return [];
99+
}
100+
101+
/**
102+
* @return array<string, mixed>
103+
*/
104+
private function convertToIndexableArray(MessageInterface $message): array
105+
{
106+
$toolsCalls = [];
107+
108+
if ($message instanceof AssistantMessage && $message->hasToolCalls()) {
109+
$toolsCalls = array_map(
110+
static fn (ToolCall $toolCall): array => $toolCall->jsonSerialize(),
111+
$message->getToolCalls(),
112+
);
113+
}
114+
115+
if ($message instanceof ToolCallMessage) {
116+
$toolsCalls = $message->getToolCall()->jsonSerialize();
117+
}
118+
119+
return [
120+
'id' => $message->getId()->toRfc4122(),
121+
'type' => $message::class,
122+
'content' => ($message instanceof SystemMessage || $message instanceof AssistantMessage || $message instanceof ToolCallMessage) ? $message->getContent() : '',
123+
'contentAsBase64' => ($message instanceof UserMessage && [] !== $message->getContent()) ? array_map(
124+
static fn (ContentInterface $content) => [
125+
'type' => $content::class,
126+
'content' => match ($content::class) {
127+
Text::class => $content->getText(),
128+
File::class,
129+
Image::class,
130+
Audio::class => $content->asBase64(),
131+
ImageUrl::class,
132+
DocumentUrl::class => $content->getUrl(),
133+
default => throw new LogicException(\sprintf('Unknown content type "%s".', $content::class)),
134+
},
135+
],
136+
$message->getContent(),
137+
) : [],
138+
'toolsCalls' => $toolsCalls,
139+
'metadata' => $message->getMetadata()->all(),
140+
];
141+
}
142+
143+
/**
144+
* @param array<string, mixed> $payload
145+
*/
146+
private function convertToMessage(array $payload): MessageInterface
147+
{
148+
$type = $payload['type'];
149+
$content = $payload['content'] ?? '';
150+
$contentAsBase64 = $payload['contentAsBase64'] ?? [];
151+
152+
$message = match ($type) {
153+
SystemMessage::class => new SystemMessage($content),
154+
AssistantMessage::class => new AssistantMessage($content, array_map(
155+
static fn (array $toolsCall): ToolCall => new ToolCall(
156+
$toolsCall['id'],
157+
$toolsCall['function']['name'],
158+
json_decode($toolsCall['function']['arguments'], true)
159+
),
160+
$payload['toolsCalls'],
161+
)),
162+
UserMessage::class => new UserMessage(...array_map(
163+
static fn (array $contentAsBase64): ContentInterface => \in_array($contentAsBase64['type'], [File::class, Image::class, Audio::class], true)
164+
? $contentAsBase64['type']::fromDataUrl($contentAsBase64['content'])
165+
: new $contentAsBase64['type']($contentAsBase64['content']),
166+
$contentAsBase64,
167+
)),
168+
ToolCallMessage::class => new ToolCallMessage(
169+
new ToolCall(
170+
$payload['toolsCalls']['id'],
171+
$payload['toolsCalls']['function']['name'],
172+
json_decode($payload['toolsCalls']['function']['arguments'], true)
173+
),
174+
$content
175+
),
176+
default => throw new LogicException(\sprintf('Unknown message type "%s".', $type)),
177+
};
178+
179+
$message->getMetadata()->set($payload['metadata']);
180+
181+
return $message;
182+
}
183+
}

0 commit comments

Comments
 (0)