diff --git a/examples/misc/persistent-chat-double-agent.php b/examples/misc/persistent-chat-double-agent.php new file mode 100644 index 00000000..81da72e3 --- /dev/null +++ b/examples/misc/persistent-chat-double-agent.php @@ -0,0 +1,45 @@ + + * + * 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); + +$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; diff --git a/src/agent/src/Agent.php b/src/agent/src/Agent.php index 6344201a..b0eb5cef 100644 --- a/src/agent/src/Agent.php +++ b/src/agent/src/Agent.php @@ -21,6 +21,9 @@ use Symfony\AI\Platform\Model; use Symfony\AI\Platform\PlatformInterface; use Symfony\AI\Platform\Result\ResultInterface; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\TimeBasedUidInterface; +use Symfony\Component\Uid\Uuid; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface; @@ -29,6 +32,8 @@ */ final readonly class Agent implements AgentInterface { + private AbstractUid&TimeBasedUidInterface $id; + /** * @var InputProcessorInterface[] */ @@ -52,6 +57,8 @@ public function __construct( ) { $this->inputProcessors = $this->initializeProcessors($inputProcessors, InputProcessorInterface::class); $this->outputProcessors = $this->initializeProcessors($outputProcessors, OutputProcessorInterface::class); + + $this->id = Uuid::v7(); } /** @@ -97,6 +104,11 @@ public function call(MessageBagInterface $messages, array $options = []): Result return $output->result; } + public function getId(): AbstractUid&TimeBasedUidInterface + { + return $this->id; + } + /** * @param InputProcessorInterface[]|OutputProcessorInterface[] $processors * @param class-string $interface diff --git a/src/agent/src/AgentInterface.php b/src/agent/src/AgentInterface.php index 83a9b748..839e7cf1 100644 --- a/src/agent/src/AgentInterface.php +++ b/src/agent/src/AgentInterface.php @@ -14,6 +14,8 @@ use Symfony\AI\Agent\Exception\ExceptionInterface; use Symfony\AI\Platform\Message\MessageBagInterface; use Symfony\AI\Platform\Result\ResultInterface; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\TimeBasedUidInterface; /** * @author Denis Zunke @@ -26,4 +28,6 @@ interface AgentInterface * @throws ExceptionInterface When the agent encounters an error (e.g., unsupported model capabilities, invalid arguments, network failures, or processor errors) */ public function call(MessageBagInterface $messages, array $options = []): ResultInterface; + + public function getId(): AbstractUid&TimeBasedUidInterface; } diff --git a/src/agent/src/Chat.php b/src/agent/src/Chat.php index 4c7ae521..32abd722 100644 --- a/src/agent/src/Chat.php +++ b/src/agent/src/Chat.php @@ -18,6 +18,9 @@ use Symfony\AI\Platform\Message\UserMessage; use Symfony\AI\Platform\Result\TextResult; +/** + * @author Christopher Hertel + */ final readonly class Chat implements ChatInterface { public function __construct( @@ -28,23 +31,25 @@ public function __construct( public function initiate(MessageBagInterface $messages): void { - $this->store->clear(); + $messages->setSession($this->agent->getId()); + + $this->store->clear($messages->getSession()); $this->store->save($messages); } public function submit(UserMessage $message): AssistantMessage { - $messages = $this->store->load(); + $messagesBag = $this->store->load($this->agent->getId()); - $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; } diff --git a/src/agent/src/Chat/MessageStore/CacheStore.php b/src/agent/src/Chat/MessageStore/CacheStore.php index 472822d7..46e40925 100644 --- a/src/agent/src/Chat/MessageStore/CacheStore.php +++ b/src/agent/src/Chat/MessageStore/CacheStore.php @@ -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)) { @@ -31,7 +32,7 @@ public function __construct( public function save(MessageBagInterface $messages): void { - $item = $this->cache->getItem($this->cacheKey); + $item = $this->cache->getItem($messages->getSession()->toRfc4122()); $item->set($messages); $item->expiresAfter($this->ttl); @@ -39,15 +40,15 @@ public function save(MessageBagInterface $messages): void $this->cache->save($item); } - public function load(): MessageBag + public function load(AbstractUid&TimeBasedUidInterface $session): MessageBagInterface { - $item = $this->cache->getItem($this->cacheKey); + $item = $this->cache->getItem($session->toRfc4122()); return $item->isHit() ? $item->get() : new MessageBag(); } - public function clear(): void + public function clear(AbstractUid&TimeBasedUidInterface $session): void { - $this->cache->deleteItem($this->cacheKey); + $this->cache->deleteItem($session->toRfc4122()); } } diff --git a/src/agent/src/Chat/MessageStore/InMemoryStore.php b/src/agent/src/Chat/MessageStore/InMemoryStore.php index b3971e11..758fca57 100644 --- a/src/agent/src/Chat/MessageStore/InMemoryStore.php +++ b/src/agent/src/Chat/MessageStore/InMemoryStore.php @@ -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->getSession()->toRfc4122()] = $messages; } - public function load(): MessageBagInterface + public function load(AbstractUid&TimeBasedUidInterface $session): MessageBagInterface { - return $this->messages ?? new MessageBag(); + return $this->messageBags[$session->toRfc4122()] ?? new MessageBag(); } - public function clear(): void + public function clear(AbstractUid&TimeBasedUidInterface $session): void { - $this->messages = new MessageBag(); + $this->messageBags[$session->toRfc4122()] = new MessageBag(); } } diff --git a/src/agent/src/Chat/MessageStore/SessionStore.php b/src/agent/src/Chat/MessageStore/SessionStore.php index 32142001..e0f2ee05 100644 --- a/src/agent/src/Chat/MessageStore/SessionStore.php +++ b/src/agent/src/Chat/MessageStore/SessionStore.php @@ -17,6 +17,8 @@ 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 { @@ -24,7 +26,6 @@ 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".'); @@ -34,16 +35,16 @@ public function __construct( public function save(MessageBagInterface $messages): void { - $this->session->set($this->sessionKey, $messages); + $this->session->set($messages->getSession()->toRfc4122(), $messages); } - public function load(): MessageBagInterface + public function load(AbstractUid&TimeBasedUidInterface $session): MessageBagInterface { - return $this->session->get($this->sessionKey, new MessageBag()); + return $this->session->get($session->toRfc4122(), new MessageBag()); } - public function clear(): void + public function clear(AbstractUid&TimeBasedUidInterface $session): void { - $this->session->remove($this->sessionKey); + $this->session->remove($session->toRfc4122()); } } diff --git a/src/agent/src/Chat/MessageStoreInterface.php b/src/agent/src/Chat/MessageStoreInterface.php index 3ab745bf..33efd465 100644 --- a/src/agent/src/Chat/MessageStoreInterface.php +++ b/src/agent/src/Chat/MessageStoreInterface.php @@ -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 $session): MessageBagInterface; - public function clear(): void; + public function clear(AbstractUid&TimeBasedUidInterface $session): void; } diff --git a/src/agent/tests/AgentTest.php b/src/agent/tests/AgentTest.php index 066cfec4..f987896f 100644 --- a/src/agent/tests/AgentTest.php +++ b/src/agent/tests/AgentTest.php @@ -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; @@ -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; @@ -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 { @@ -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())); + } } diff --git a/src/platform/src/Message/MessageBag.php b/src/platform/src/Message/MessageBag.php index 87ad0c5b..d7af2a83 100644 --- a/src/platform/src/Message/MessageBag.php +++ b/src/platform/src/Message/MessageBag.php @@ -11,6 +11,10 @@ namespace Symfony\AI\Platform\Message; +use Symfony\AI\Platform\Exception\RuntimeException; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\TimeBasedUidInterface; + /** * @final * @@ -23,6 +27,8 @@ class MessageBag implements MessageBagInterface */ private array $messages; + private (AbstractUid&TimeBasedUidInterface)|null $session = null; + public function __construct(MessageInterface ...$messages) { $this->messages = array_values($messages); @@ -109,6 +115,20 @@ public function containsImage(): bool return false; } + public function setSession(AbstractUid&TimeBasedUidInterface $session): void + { + $this->session = $session; + } + + public function getSession(): AbstractUid&TimeBasedUidInterface + { + if (!$this->session instanceof TimeBasedUidInterface) { + throw new RuntimeException('Current message bag session is not set.'); + } + + return $this->session; + } + public function count(): int { return \count($this->messages); diff --git a/src/platform/src/Message/MessageBagInterface.php b/src/platform/src/Message/MessageBagInterface.php index 070c5196..ed8c8eb0 100644 --- a/src/platform/src/Message/MessageBagInterface.php +++ b/src/platform/src/Message/MessageBagInterface.php @@ -11,8 +11,12 @@ namespace Symfony\AI\Platform\Message; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\TimeBasedUidInterface; + /** * @author Oskar Stark + * @author Christopher Hertel */ interface MessageBagInterface extends \Countable { @@ -36,4 +40,8 @@ public function prepend(MessageInterface $message): self; public function containsAudio(): bool; public function containsImage(): bool; + + public function setSession(AbstractUid&TimeBasedUidInterface $session): void; + + public function getSession(): AbstractUid&TimeBasedUidInterface; }