Skip to content

Commit 8be61b1

Browse files
committed
feature #797 [Chat] Add Redis as message store (Guikingone)
This PR was merged into the main branch. Discussion ---------- [Chat] Add Redis as message store | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | yes | Issues | Improve #16 | License | MIT Hi 👋🏻 This PR aims to introduce the support for `Redis` as a message store, the transformation is done through the `Serializer` component (and will probably be used widely later for existing message stores) and a custom normalizer. Commits ------- 727a06e feat(chat): redis added
2 parents 3b12db8 + 727a06e commit 8be61b1

File tree

17 files changed

+522
-10
lines changed

17 files changed

+522
-10
lines changed

docs/components/chat.rst

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

4243
Supported Message stores
4344
------------------------
@@ -47,6 +48,7 @@ Supported Message stores
4748
* `InMemory`_
4849
* `Meilisearch`_
4950
* `Pogocache`_
51+
* `Redis`_
5052

5153
Implementing a Bridge
5254
---------------------
@@ -127,8 +129,10 @@ store and ``bin/console ai:message-store:drop`` to clean up the message store:
127129
.. _`Current process context storage with InMemory`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat.php
128130
.. _`Long-term context with Meilisearch`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-meilisearch.php
129131
.. _`Long-term context with Pogocache`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-pogocache.php
132+
.. _`Long-term context with Redis`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-redis.php
130133
.. _`Cache`: https://symfony.com/doc/current/components/cache.html
131134
.. _`InMemory`: https://www.php.net/manual/en/language.types.array.php
132135
.. _`HttpFoundation session`: https://developers.cloudflare.com/vectorize/
133136
.. _`Meilisearch`: https://www.meilisearch.com/
134137
.. _`Pogocache`: https://pogocache.com/
138+
.. _`Redis`: https://redis.io/

examples/.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,5 +169,5 @@ SUPABASE_MATCH_FUNCTION=match_documents
169169
POGOCACHE_HOST=http://127.0.0.1:9401
170170
POGOCACHE_PASSWORD=symfony
171171

172-
# Redis (store)
172+
# Redis (both store and message store)
173173
REDIS_HOST=localhost
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Redis\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+
$redis = new Redis([
24+
'host' => env('REDIS_HOST'),
25+
'port' => 6379,
26+
]);
27+
28+
$store = new MessageStore($redis, 'symfony');
29+
$store->setup();
30+
31+
$agent = new Agent($platform, 'gpt-4o-mini');
32+
$chat = new Chat($agent, $store);
33+
34+
$messages = new MessageBag(
35+
Message::forSystem('You are a helpful assistant. You only answer with short sentences.'),
36+
);
37+
38+
$chat->initiate($messages);
39+
$chat->submit(Message::ofUser('My name is Christopher.'));
40+
$message = $chat->submit(Message::ofUser('What is my name?'));
41+
42+
echo $message->getContent().\PHP_EOL;

examples/commands/message-stores.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use Symfony\AI\Chat\Bridge\Local\InMemoryStore;
1717
use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore as MeilisearchMessageStore;
1818
use Symfony\AI\Chat\Bridge\Pogocache\MessageStore as PogocacheMessageStore;
19+
use Symfony\AI\Chat\Bridge\Redis\MessageStore as RedisMessageStore;
1920
use Symfony\AI\Chat\Command\DropStoreCommand;
2021
use Symfony\AI\Chat\Command\SetupStoreCommand;
22+
use Symfony\AI\Chat\MessageNormalizer;
2123
use Symfony\Component\Cache\Adapter\ArrayAdapter;
2224
use Symfony\Component\Clock\MonotonicClock;
2325
use Symfony\Component\Console\Application;
@@ -28,6 +30,9 @@
2830
use Symfony\Component\HttpFoundation\RequestStack;
2931
use Symfony\Component\HttpFoundation\Session\Session;
3032
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
33+
use Symfony\Component\Serializer\Encoder\JsonEncoder;
34+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
35+
use Symfony\Component\Serializer\Serializer;
3136

3237
$factories = [
3338
'cache' => static fn (): CacheStore => new CacheStore(new ArrayAdapter(), cacheKey: 'symfony'),
@@ -45,6 +50,15 @@
4550
env('POGOCACHE_PASSWORD'),
4651
'symfony',
4752
),
53+
'redis' => static fn (): RedisMessageStore => new RedisMessageStore(new Redis([
54+
'host' => env('REDIS_HOST'),
55+
'port' => 6379,
56+
]), 'symfony', new Serializer([
57+
new ArrayDenormalizer(),
58+
new MessageNormalizer(),
59+
], [
60+
new JsonEncoder(),
61+
])),
4862
'session' => static function (): SessionStore {
4963
$request = Request::create('/');
5064
$request->setSession(new Session(new MockArraySessionStorage()));

examples/commands/stores.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,10 @@
8989
env('QDRANT_SERVICE_API_KEY'),
9090
'symfony',
9191
),
92-
'redis' => static fn (): RedisStore => new RedisStore(
93-
new Redis([
94-
'host' => env('REDIS_HOST'),
95-
'port' => 6379,
96-
]),
97-
'symfony'
98-
),
92+
'redis' => static fn (): RedisStore => new RedisStore(new Redis([
93+
'host' => env('REDIS_HOST'),
94+
'port' => 6379,
95+
]), 'symfony'),
9996
'surrealdb' => static fn (): SurrealDbStore => new SurrealDbStore(
10097
httpClient: http_client(),
10198
endpointUrl: env('SURREALDB_HOST'),

examples/compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ services:
133133
- '6333:6333'
134134

135135
redis:
136-
image: redis:8.0.3
136+
image: redis:8.2.2-alpine
137137
ports:
138138
- '6379:6379'
139139

examples/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"symfony/finder": "^7.3|^8.0",
3434
"symfony/http-foundation": "^7.3|^8.0",
3535
"symfony/process": "^7.3|^8.0",
36+
"symfony/serializer": "^7.3|^8.0",
3637
"symfony/var-dumper": "^7.3|^8.0"
3738
},
3839
"require-dev": {

src/ai-bundle/config/options.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,31 @@
765765
->end()
766766
->end()
767767
->end()
768+
->arrayNode('redis')
769+
->useAttributeAsKey('name')
770+
->arrayPrototype()
771+
->children()
772+
->variableNode('connection_parameters')
773+
->info('see https://github.com/phpredis/phpredis?tab=readme-ov-file#example-1')
774+
->cannotBeEmpty()
775+
->end()
776+
->stringNode('client')
777+
->info('a service id of a Redis client')
778+
->cannotBeEmpty()
779+
->end()
780+
->stringNode('endpoint')->cannotBeEmpty()->end()
781+
->stringNode('index_name')->cannotBeEmpty()->end()
782+
->end()
783+
->validate()
784+
->ifTrue(static fn (array $v): bool => !isset($v['connection_parameters']) && !isset($v['client']))
785+
->thenInvalid('Either "connection_parameters" or "client" must be configured.')
786+
->end()
787+
->validate()
788+
->ifTrue(static fn (array $v): bool => isset($v['connection_parameters']) && isset($v['client']))
789+
->thenInvalid('Either "connection_parameters" or "client" can be configured, but not both.')
790+
->end()
791+
->end()
792+
->end()
768793
->arrayNode('session')
769794
->useAttributeAsKey('name')
770795
->arrayPrototype()

src/ai-bundle/config/services.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\AI\AiBundle\Security\EventListener\IsGrantedToolAttributeListener;
2424
use Symfony\AI\Chat\Command\DropStoreCommand as DropMessageStoreCommand;
2525
use Symfony\AI\Chat\Command\SetupStoreCommand as SetupMessageStoreCommand;
26+
use Symfony\AI\Chat\MessageNormalizer;
2627
use Symfony\AI\Platform\Bridge\AiMlApi\ModelCatalog as AiMlApiModelCatalog;
2728
use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract;
2829
use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog;
@@ -182,6 +183,10 @@
182183
// search result processors
183184
->set('ai.platform.search_result_processor.perplexity', PerplexitySearchResultProcessor::class)
184185

186+
// serializer
187+
->set('ai.chat.message_bag.normalizer', MessageNormalizer::class)
188+
->tag('serializer.normalizer')
189+
185190
// commands
186191
->set('ai.command.chat', AgentCallCommand::class)
187192
->args([

src/ai-bundle/src/AiBundle.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use Symfony\AI\Chat\Bridge\HttpFoundation\SessionStore;
3838
use Symfony\AI\Chat\Bridge\Meilisearch\MessageStore as MeilisearchMessageStore;
3939
use Symfony\AI\Chat\Bridge\Pogocache\MessageStore as PogocacheMessageStore;
40+
use Symfony\AI\Chat\Bridge\Redis\MessageStore as RedisMessageStore;
4041
use Symfony\AI\Chat\MessageStoreInterface;
4142
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
4243
use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory;
@@ -1393,6 +1394,30 @@ private function processMessageStoreConfig(string $type, array $messageStores, C
13931394
}
13941395
}
13951396

1397+
if ('redis' === $type) {
1398+
foreach ($messageStores as $name => $messageStore) {
1399+
if (isset($messageStore['client'])) {
1400+
$redisClient = new Reference($messageStore['client']);
1401+
} else {
1402+
$redisClient = new Definition(\Redis::class);
1403+
$redisClient->setArguments([$messageStore['connection_parameters']]);
1404+
}
1405+
1406+
$definition = new Definition(RedisMessageStore::class);
1407+
$definition
1408+
->setArguments([
1409+
$redisClient,
1410+
$messageStore['index_name'],
1411+
new Reference('serializer'),
1412+
])
1413+
->addTag('ai.message_store');
1414+
1415+
$container->setDefinition('ai.message_store.'.$type.'.'.$name, $definition);
1416+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $name);
1417+
$container->registerAliasForArgument('ai.message_store.'.$type.'.'.$name, MessageStoreInterface::class, $type.'_'.$name);
1418+
}
1419+
}
1420+
13961421
if ('session' === $type) {
13971422
foreach ($messageStores as $name => $messageStore) {
13981423
$definition = new Definition(SessionStore::class);

0 commit comments

Comments
 (0)