Skip to content

[Agent] Add MessageBag "session" #254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
45 changes: 45 additions & 0 deletions examples/misc/persistent-chat-double-agent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\Agent\Chat;
use Symfony\AI\Agent\Chat\MessageStore\InMemoryStore;
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
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());
$llm = new Gpt(Gpt::GPT_4O_MINI);

$firstAgent = new Agent($platform, $llm, logger: logger());
$secondAgent = new Agent($platform, $llm, logger: logger());

$store = new InMemoryStore();

$firstChat = new Chat($firstAgent, $store);
$secondChat = new Chat($secondAgent, $store);
Comment on lines +25 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could go with this, can't we?

Suggested change
$firstAgent = new Agent($platform, $llm, logger: logger());
$secondAgent = new Agent($platform, $llm, logger: logger());
$store = new InMemoryStore();
$firstChat = new Chat($firstAgent, $store);
$secondChat = new Chat($secondAgent, $store);
$agent = new Agent($platform, $llm, logger: logger());
$store = new InMemoryStore();
$firstChat = new Chat($agent, $store);
$secondChat = new Chat($agent, $store);


$firstChat->initiate(new MessageBag(
Message::forSystem('You are a helpful assistant. You only answer with short sentences.'),
));
$secondChat->initiate(new MessageBag(
Message::forSystem('You are a helpful assistant. You only answer with short sentences.'),
));

$firstChat->submit(Message::ofUser('My name is Christopher.'));
$firstChatMessage = $firstChat->submit(Message::ofUser('What is my name?'));
$secondChatMessage = $secondChat->submit(Message::ofUser('What is my name?'));

echo $firstChatMessage->content.\PHP_EOL;
echo $secondChatMessage->content.\PHP_EOL;
19 changes: 14 additions & 5 deletions src/agent/src/Chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@
use Symfony\AI\Platform\Message\MessageBagInterface;
use Symfony\AI\Platform\Message\UserMessage;
use Symfony\AI\Platform\Result\TextResult;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\TimeBasedUidInterface;

/**
* @author Christopher Hertel <[email protected]>
*/
final readonly class Chat implements ChatInterface
{
private AbstractUid&TimeBasedUidInterface $currentMessageBag;

public function __construct(
private AgentInterface $agent,
private MessageStoreInterface $store,
Expand All @@ -30,21 +37,23 @@ public function initiate(MessageBagInterface $messages): void
{
$this->store->clear();
$this->store->save($messages);

$this->currentMessageBag = $messages->getId();
}

public function submit(UserMessage $message): AssistantMessage
{
$messages = $this->store->load();
$messagesBag = $this->store->load($this->currentMessageBag);

$messages->add($message);
$result = $this->agent->call($messages);
$messagesBag->add($message);
$result = $this->agent->call($messagesBag);

\assert($result instanceof TextResult);

$assistantMessage = Message::ofAssistant($result->getContent());
$messages->add($assistantMessage);
$messagesBag->add($assistantMessage);

$this->store->save($messages);
$this->store->save($messagesBag);

return $assistantMessage;
}
Expand Down
11 changes: 6 additions & 5 deletions src/agent/src/Chat/MessageStore/CacheStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
use Symfony\AI\Agent\Exception\RuntimeException;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageBagInterface;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\TimeBasedUidInterface;

final readonly class CacheStore implements MessageStoreInterface
{
public function __construct(
private CacheItemPoolInterface $cache,
private string $cacheKey,
private int $ttl = 86400,
) {
if (!interface_exists(CacheItemPoolInterface::class)) {
Expand All @@ -31,23 +32,23 @@ public function __construct(

public function save(MessageBagInterface $messages): void
{
$item = $this->cache->getItem($this->cacheKey);
$item = $this->cache->getItem($messages->getId()->toRfc4122());

$item->set($messages);
$item->expiresAfter($this->ttl);

$this->cache->save($item);
}

public function load(): MessageBag
public function load(AbstractUid&TimeBasedUidInterface $id): MessageBagInterface
{
$item = $this->cache->getItem($this->cacheKey);
$item = $this->cache->getItem($id->toRfc4122());

return $item->isHit() ? $item->get() : new MessageBag();
}

public function clear(): void
{
$this->cache->deleteItem($this->cacheKey);
$this->cache->clear();
}
}
15 changes: 10 additions & 5 deletions src/agent/src/Chat/MessageStore/InMemoryStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,28 @@
use Symfony\AI\Agent\Chat\MessageStoreInterface;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\MessageBagInterface;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\TimeBasedUidInterface;

final class InMemoryStore implements MessageStoreInterface
{
private MessageBagInterface $messages;
/**
* @var MessageBagInterface[]
*/
private array $messageBags;

public function save(MessageBagInterface $messages): void
{
$this->messages = $messages;
$this->messageBags[$messages->getId()->toRfc4122()] = $messages;
}

public function load(): MessageBagInterface
public function load(AbstractUid&TimeBasedUidInterface $id): MessageBagInterface
{
return $this->messages ?? new MessageBag();
return $this->messageBags[$id->toRfc4122()] ?? new MessageBag();
}

public function clear(): void
{
$this->messages = new MessageBag();
$this->messageBags = [];
}
}
12 changes: 7 additions & 5 deletions src/agent/src/Chat/MessageStore/SessionStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,35 @@
use Symfony\AI\Platform\Message\MessageBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\TimeBasedUidInterface;

final readonly class SessionStore implements MessageStoreInterface
{
private SessionInterface $session;

public function __construct(
RequestStack $requestStack,
private string $sessionKey = 'messages',
) {
if (!class_exists(RequestStack::class)) {
throw new RuntimeException('For using the SessionStore as message store, the symfony/http-foundation package is required. Try running "composer require symfony/http-foundation".');
}

$this->session = $requestStack->getSession();
}

public function save(MessageBagInterface $messages): void
{
$this->session->set($this->sessionKey, $messages);
$this->session->set($messages->getId()->toRfc4122(), $messages);
}

public function load(): MessageBagInterface
public function load(AbstractUid&TimeBasedUidInterface $id): MessageBagInterface
{
return $this->session->get($this->sessionKey, new MessageBag());
return $this->session->get($id->toRfc4122(), new MessageBag());
}

public function clear(): void
{
$this->session->remove($this->sessionKey);
$this->session->clear();
}
}
4 changes: 3 additions & 1 deletion src/agent/src/Chat/MessageStoreInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
namespace Symfony\AI\Agent\Chat;

use Symfony\AI\Platform\Message\MessageBagInterface;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\TimeBasedUidInterface;

interface MessageStoreInterface
{
public function save(MessageBagInterface $messages): void;

public function load(): MessageBagInterface;
public function load(AbstractUid&TimeBasedUidInterface $id): MessageBagInterface;

public function clear(): void;
}
33 changes: 33 additions & 0 deletions src/agent/tests/AgentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use Symfony\AI\Agent\Agent;
use Symfony\AI\Agent\AgentAwareInterface;
use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Agent\Chat;
use Symfony\AI\Agent\Chat\MessageStore\InMemoryStore;
use Symfony\AI\Agent\Exception\InvalidArgumentException;
use Symfony\AI\Agent\Exception\MissingModelSupportException;
use Symfony\AI\Agent\Exception\RuntimeException;
Expand All @@ -30,6 +32,7 @@
use Symfony\AI\Platform\Message\Content\Audio;
use Symfony\AI\Platform\Message\Content\Image;
use Symfony\AI\Platform\Message\Content\Text;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\UserMessage;
use Symfony\AI\Platform\Model;
Expand All @@ -49,6 +52,8 @@
#[UsesClass(Text::class)]
#[UsesClass(Audio::class)]
#[UsesClass(Image::class)]
#[UsesClass(InMemoryStore::class)]
#[UsesClass(Chat::class)]
#[Small]
final class AgentTest extends TestCase
{
Expand Down Expand Up @@ -395,4 +400,32 @@ public function testConstructorAcceptsTraversableProcessors()

$this->assertInstanceOf(AgentInterface::class, $agent);
}

public function testDoubleAgentCanUseSameMessageStore()
{
$platform = $this->createMock(PlatformInterface::class);
$model = $this->createMock(Model::class);

$firstAgent = new Agent($platform, $model);
$secondAgent = new Agent($platform, $model);

$store = new InMemoryStore();

$firstChat = new Chat($firstAgent, $store);
$secondChat = new Chat($secondAgent, $store);

$messages = new MessageBag(
Message::forSystem('You are a helpful assistant. You only answer with short sentences.'),
);

$firstChat->initiate($messages);
$firstChat->submit(new UserMessage(new Text('Hello')));

$secondChat->initiate($messages);
$secondChat->submit(new UserMessage(new Text('Hello')));
$secondChat->submit(new UserMessage(new Text('Hello there')));

$this->assertCount(1, $store->load($firstAgent->getId()));
$this->assertCount(2, $store->load($secondAgent->getId()));
}
}
71 changes: 71 additions & 0 deletions src/agent/tests/Chat/InMemoryStoreTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Agent\Tests\Chat;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Agent\Chat\MessageStore\InMemoryStore;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

#[CoversClass(InMemoryStore::class)]
#[UsesClass(MessageBag::class)]
#[UsesClass(Message::class)]
final class InMemoryStoreTest extends TestCase
{
public function testItCanStore()
{
$messageBag = new MessageBag();
$messageBag->add(Message::ofUser('Hello'));

$store = new InMemoryStore();
$store->save($messageBag);

$this->assertCount(1, $store->load($messageBag->getId()));
}

public function testItCanStoreMultipleMessageBags()
{
$firstMessageBag = new MessageBag();
$firstMessageBag->add(Message::ofUser('Hello'));

$secondMessageBag = new MessageBag();

$store = new InMemoryStore();
$store->save($firstMessageBag);
$store->save($secondMessageBag);

$this->assertCount(1, $store->load($firstMessageBag->getId()));
$this->assertCount(0, $store->load($secondMessageBag->getId()));
}

public function testItCanClear()
{
$firstMessageBag = new MessageBag();
$firstMessageBag->add(Message::ofUser('Hello'));

$secondMessageBag = new MessageBag();

$store = new InMemoryStore();
$store->save($firstMessageBag);
$store->save($secondMessageBag);

$this->assertCount(1, $store->load($firstMessageBag->getId()));
$this->assertCount(0, $store->load($secondMessageBag->getId()));

$store->clear();

$this->assertCount(0, $store->load($firstMessageBag->getId()));
$this->assertCount(0, $store->load($secondMessageBag->getId()));
}
}
27 changes: 27 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,33 @@
->end()
->end()
->end()
->arrayNode('message_store')
->children()
->arrayNode('cache')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('service')->cannotBeEmpty()->defaultValue('cache.app')->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('chat')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('agent')
->info('Name of the agent used for the chat')
->end()
->scalarNode('message_store')
->info('Name of the message store')
->end()
->end()
->end()
->end()
->end()
;
};
Loading
Loading