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 @@ -38,6 +38,7 @@ You can find more advanced usage in combination with an Agent using the store fo
* `Current process context storage with InMemory`_
* `Long-term context with Meilisearch`_
* `Long-term context with Pogocache`_
* `Long-term context with Redis`_

Supported Message stores
------------------------
Expand All @@ -47,6 +48,7 @@ Supported Message stores
* `InMemory`_
* `Meilisearch`_
* `Pogocache`_
* `Redis`_

Implementing a Bridge
---------------------
Expand Down Expand Up @@ -127,8 +129,10 @@ store and ``bin/console ai:message-store:drop`` to clean up the message store:
.. _`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
.. _`Long-term context with Redis`: https://github.com/symfony/ai/blob/main/examples/chat/persistent-chat-redis.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/
.. _`Redis`: https://redis.io/
2 changes: 1 addition & 1 deletion examples/.env
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,5 @@ SUPABASE_MATCH_FUNCTION=match_documents
POGOCACHE_HOST=http://127.0.0.1:9401
POGOCACHE_PASSWORD=symfony

# Redis (store)
# Redis (both store and message store)
REDIS_HOST=localhost
42 changes: 42 additions & 0 deletions examples/chat/persistent-chat-redis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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\Redis\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());

$redis = new Redis([
'host' => env('REDIS_HOST'),
'port' => 6379,
]);

$store = new MessageStore($redis, 'symfony');
$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;
14 changes: 14 additions & 0 deletions examples/commands/message-stores.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
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\Bridge\Redis\MessageStore as RedisMessageStore;
use Symfony\AI\Chat\Command\DropStoreCommand;
use Symfony\AI\Chat\Command\SetupStoreCommand;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Clock\MonotonicClock;
use Symfony\Component\Console\Application;
Expand All @@ -28,6 +30,9 @@
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Serializer;

$factories = [
'cache' => static fn (): CacheStore => new CacheStore(new ArrayAdapter(), cacheKey: 'symfony'),
Expand All @@ -45,6 +50,15 @@
env('POGOCACHE_PASSWORD'),
'symfony',
),
'redis' => static fn (): RedisMessageStore => new RedisMessageStore(new Redis([
'host' => env('REDIS_HOST'),
'port' => 6379,
]), 'symfony', new Serializer([
new ArrayDenormalizer(),
new MessageNormalizer(),
], [
new JsonEncoder(),
])),
'session' => static function (): SessionStore {
$request = Request::create('/');
$request->setSession(new Session(new MockArraySessionStorage()));
Expand Down
11 changes: 4 additions & 7 deletions examples/commands/stores.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,10 @@
env('QDRANT_SERVICE_API_KEY'),
'symfony',
),
'redis' => static fn (): RedisStore => new RedisStore(
new Redis([
'host' => env('REDIS_HOST'),
'port' => 6379,
]),
'symfony'
),
'redis' => static fn (): RedisStore => new RedisStore(new Redis([
'host' => env('REDIS_HOST'),
'port' => 6379,
]), 'symfony'),
'surrealdb' => static fn (): SurrealDbStore => new SurrealDbStore(
httpClient: http_client(),
endpointUrl: env('SURREALDB_HOST'),
Expand Down
2 changes: 1 addition & 1 deletion examples/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ services:
- '6333:6333'

redis:
image: redis:8.0.3
image: redis:8.2.2-alpine
ports:
- '6379:6379'

Expand Down
1 change: 1 addition & 0 deletions examples/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"symfony/finder": "^7.3|^8.0",
"symfony/http-foundation": "^7.3|^8.0",
"symfony/process": "^7.3|^8.0",
"symfony/serializer": "^7.3|^8.0",
"symfony/var-dumper": "^7.3|^8.0"
},
"require-dev": {
Expand Down
25 changes: 25 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,31 @@
->end()
->end()
->end()
->arrayNode('redis')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->variableNode('connection_parameters')
->info('see https://github.com/phpredis/phpredis?tab=readme-ov-file#example-1')
->cannotBeEmpty()
->end()
->stringNode('client')
->info('a service id of a Redis client')
->cannotBeEmpty()
->end()
->stringNode('endpoint')->cannotBeEmpty()->end()
->stringNode('index_name')->cannotBeEmpty()->end()
->end()
->validate()
->ifTrue(static fn (array $v): bool => !isset($v['connection_parameters']) && !isset($v['client']))
->thenInvalid('Either "connection_parameters" or "client" must be configured.')
->end()
->validate()
->ifTrue(static fn (array $v): bool => isset($v['connection_parameters']) && isset($v['client']))
->thenInvalid('Either "connection_parameters" or "client" can be configured, but not both.')
->end()
->end()
->end()
->arrayNode('session')
->useAttributeAsKey('name')
->arrayPrototype()
Expand Down
5 changes: 5 additions & 0 deletions src/ai-bundle/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Symfony\AI\AiBundle\Security\EventListener\IsGrantedToolAttributeListener;
use Symfony\AI\Chat\Command\DropStoreCommand as DropMessageStoreCommand;
use Symfony\AI\Chat\Command\SetupStoreCommand as SetupMessageStoreCommand;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\AI\Platform\Bridge\AiMlApi\ModelCatalog as AiMlApiModelCatalog;
use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract;
use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog;
Expand Down Expand Up @@ -182,6 +183,10 @@
// search result processors
->set('ai.platform.search_result_processor.perplexity', PerplexitySearchResultProcessor::class)

// serializer
->set('ai.chat.message_bag.normalizer', MessageNormalizer::class)
->tag('serializer.normalizer')

// commands
->set('ai.command.chat', AgentCallCommand::class)
->args([
Expand Down
25 changes: 25 additions & 0 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
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\Bridge\Redis\MessageStore as RedisMessageStore;
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 @@ -1393,6 +1394,30 @@ private function processMessageStoreConfig(string $type, array $messageStores, C
}
}

if ('redis' === $type) {
foreach ($messageStores as $name => $messageStore) {
if (isset($messageStore['client'])) {
$redisClient = new Reference($messageStore['client']);
} else {
$redisClient = new Definition(\Redis::class);
$redisClient->setArguments([$messageStore['connection_parameters']]);
}

$definition = new Definition(RedisMessageStore::class);
$definition
->setArguments([
$redisClient,
$messageStore['index_name'],
new Reference('serializer'),
])
->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
16 changes: 16 additions & 0 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ public function testMessageStoreCommandsAreDefined()
$this->assertArrayHasKey('console.command', $dropStoreCommandDefinition->getTags());
}

public function testMessageBagNormalizerIsRegistered()
{
$container = $this->buildContainer($this->getFullConfig());

$this->assertTrue($container->hasDefinition('ai.chat.message_bag.normalizer'));
}

public function testInjectionAgentAliasIsRegistered()
{
$container = $this->buildContainer([
Expand Down Expand Up @@ -3058,6 +3065,15 @@ private function getFullConfig(): array
'key' => 'bar',
],
],
'redis' => [
'my_redis_store' => [
'connection_parameters' => [
'host' => '1.2.3.4',
'port' => 6379,
],
'index_name' => 'my_message_store',
],
],
'session' => [
'my_session_message_store' => [
'identifier' => 'session',
Expand Down
4 changes: 4 additions & 0 deletions src/chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ CHANGELOG
---

* Introduce the component
* Add support for external message stores:
- Meilisearch
- Pogocache
- Redis
4 changes: 3 additions & 1 deletion src/chat/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@
"symfony/ai-platform": "@dev"
},
"require-dev": {
"ext-redis": "*",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^11.5.13",
"psr/cache": "^3.0",
"symfony/dependency-injection": "^7.3|^8.0",
"symfony/clock": "^7.3|^8.0",
"symfony/console": "^7.3|^8.0",
"symfony/http-foundation": "^7.3|^8.0",
"psr/cache": "^3.0"
"symfony/serializer": "^7.3|^8.0"
},
"suggest": {
"symfony/clock": "To use stores that requires waiting for actions like Meilisearch"
Expand Down
62 changes: 62 additions & 0 deletions src/chat/src/Bridge/Redis/MessageStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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\Redis;

use Symfony\AI\Chat\ManagedStoreInterface;
use Symfony\AI\Chat\MessageNormalizer;
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;

/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class MessageStore implements ManagedStoreInterface, MessageStoreInterface
{
public function __construct(
private readonly \Redis $redis,
private readonly string $indexName,
private readonly SerializerInterface $serializer = new Serializer([
new ArrayDenormalizer(),
new MessageNormalizer(),
], [new JsonEncoder()]),
) {
}

public function setup(array $options = []): void
{
if ($this->redis->exists($this->indexName)) {
return;
}

$this->redis->set($this->indexName, $this->serializer->serialize([], 'json'));
}

public function drop(): void
{
$this->redis->set($this->indexName, $this->serializer->serialize([], 'json'));
}

public function save(MessageBag $messages): void
{
$this->redis->set($this->indexName, $this->serializer->serialize($messages->getMessages(), 'json'));
}

public function load(): MessageBag
{
return new MessageBag(...$this->serializer->deserialize($this->redis->get($this->indexName), MessageInterface::class.'[]', 'json'));
}
}
Loading