mirror of
https://github.com/symfony/ai-agent.git
synced 2026-03-23 23:12:11 +01:00
refactor(core): chat sub-component started
This commit is contained in:
@@ -11,7 +11,6 @@ Framework for building AI agents with user interaction and task execution. Built
|
||||
### Core Classes
|
||||
- **Agent** (`src/Agent.php`): Main orchestration class
|
||||
- **AgentInterface**: Contract for implementations
|
||||
- **Chat** (`src/Chat.php`): High-level conversation interface
|
||||
- **Input/Output** (`src/Input.php`, `src/Output.php`): Pipeline data containers
|
||||
|
||||
### Processing Pipeline
|
||||
@@ -79,4 +78,4 @@ cd ../../.. && vendor/bin/php-cs-fixer fix src/agent/
|
||||
- Component is experimental (BC breaks possible)
|
||||
- Add `@author` tags to new classes
|
||||
- Use component-specific exceptions from `src/Exception/`
|
||||
- Follow `@Symfony` PHP CS Fixer rules
|
||||
- Follow `@Symfony` PHP CS Fixer rules
|
||||
|
||||
54
src/Chat.php
54
src/Chat.php
@@ -1,54 +0,0 @@
|
||||
<?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\Agent;
|
||||
|
||||
use Symfony\AI\Agent\Chat\MessageStoreInterface;
|
||||
use Symfony\AI\Platform\Message\AssistantMessage;
|
||||
use Symfony\AI\Platform\Message\Message;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
use Symfony\AI\Platform\Message\UserMessage;
|
||||
use Symfony\AI\Platform\Result\TextResult;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final readonly class Chat implements ChatInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AgentInterface $agent,
|
||||
private MessageStoreInterface $store,
|
||||
) {
|
||||
}
|
||||
|
||||
public function initiate(MessageBag $messages): void
|
||||
{
|
||||
$this->store->clear();
|
||||
$this->store->save($messages);
|
||||
}
|
||||
|
||||
public function submit(UserMessage $message): AssistantMessage
|
||||
{
|
||||
$messages = $this->store->load();
|
||||
|
||||
$messages->add($message);
|
||||
$result = $this->agent->call($messages);
|
||||
|
||||
\assert($result instanceof TextResult);
|
||||
|
||||
$assistantMessage = Message::ofAssistant($result->getContent());
|
||||
$messages->add($assistantMessage);
|
||||
|
||||
$this->store->save($messages);
|
||||
|
||||
return $assistantMessage;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?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\Agent\Chat\MessageStore;
|
||||
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\AI\Agent\Chat\MessageStoreInterface;
|
||||
use Symfony\AI\Agent\Exception\RuntimeException;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final readonly class CacheStore implements MessageStoreInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CacheItemPoolInterface $cache,
|
||||
private string $cacheKey,
|
||||
private int $ttl = 86400,
|
||||
) {
|
||||
if (!interface_exists(CacheItemPoolInterface::class)) {
|
||||
throw new RuntimeException('For using the CacheStore as message store, a PSR-6 cache implementation is required. Try running "composer require symfony/cache" or another PSR-6 compatible cache.');
|
||||
}
|
||||
}
|
||||
|
||||
public function save(MessageBag $messages): void
|
||||
{
|
||||
$item = $this->cache->getItem($this->cacheKey);
|
||||
|
||||
$item->set($messages);
|
||||
$item->expiresAfter($this->ttl);
|
||||
|
||||
$this->cache->save($item);
|
||||
}
|
||||
|
||||
public function load(): MessageBag
|
||||
{
|
||||
$item = $this->cache->getItem($this->cacheKey);
|
||||
|
||||
return $item->isHit() ? $item->get() : new MessageBag();
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->cache->deleteItem($this->cacheKey);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?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\Agent\Chat\MessageStore;
|
||||
|
||||
use Symfony\AI\Agent\Chat\MessageStoreInterface;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class InMemoryStore implements MessageStoreInterface
|
||||
{
|
||||
private MessageBag $messages;
|
||||
|
||||
public function save(MessageBag $messages): void
|
||||
{
|
||||
$this->messages = $messages;
|
||||
}
|
||||
|
||||
public function load(): MessageBag
|
||||
{
|
||||
return $this->messages ?? new MessageBag();
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->messages = new MessageBag();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?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\Agent\Chat\MessageStore;
|
||||
|
||||
use Symfony\AI\Agent\Chat\MessageStoreInterface;
|
||||
use Symfony\AI\Agent\Exception\RuntimeException;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
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(MessageBag $messages): void
|
||||
{
|
||||
$this->session->set($this->sessionKey, $messages);
|
||||
}
|
||||
|
||||
public function load(): MessageBag
|
||||
{
|
||||
return $this->session->get($this->sessionKey, new MessageBag());
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->session->remove($this->sessionKey);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?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\Agent\Chat;
|
||||
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
interface MessageStoreInterface
|
||||
{
|
||||
public function save(MessageBag $messages): void;
|
||||
|
||||
public function load(): MessageBag;
|
||||
|
||||
public function clear(): void;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?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\Agent;
|
||||
|
||||
use Symfony\AI\Agent\Exception\ExceptionInterface;
|
||||
use Symfony\AI\Platform\Message\AssistantMessage;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
use Symfony\AI\Platform\Message\UserMessage;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
interface ChatInterface
|
||||
{
|
||||
public function initiate(MessageBag $messages): void;
|
||||
|
||||
/**
|
||||
* @throws ExceptionInterface When the chat submission fails due to agent errors
|
||||
*/
|
||||
public function submit(UserMessage $message): AssistantMessage;
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
<?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\Agent\Tests;
|
||||
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Agent\AgentInterface;
|
||||
use Symfony\AI\Agent\Chat;
|
||||
use Symfony\AI\Agent\Chat\MessageStoreInterface;
|
||||
use Symfony\AI\Platform\Message\AssistantMessage;
|
||||
use Symfony\AI\Platform\Message\Message;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
use Symfony\AI\Platform\Result\TextResult;
|
||||
|
||||
final class ChatTest extends TestCase
|
||||
{
|
||||
private AgentInterface&MockObject $agent;
|
||||
private MessageStoreInterface&MockObject $store;
|
||||
private Chat $chat;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->agent = $this->createMock(AgentInterface::class);
|
||||
$this->store = $this->createMock(MessageStoreInterface::class);
|
||||
$this->chat = new Chat($this->agent, $this->store);
|
||||
}
|
||||
|
||||
public function testItInitiatesChatByClearingAndSavingMessages()
|
||||
{
|
||||
$messages = $this->createMock(MessageBag::class);
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('clear');
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('save')
|
||||
->with($messages);
|
||||
|
||||
$this->chat->initiate($messages);
|
||||
}
|
||||
|
||||
public function testItSubmitsUserMessageAndReturnsAssistantMessage()
|
||||
{
|
||||
$userMessage = Message::ofUser('Hello, how are you?');
|
||||
$existingMessages = new MessageBag();
|
||||
$assistantContent = 'I am doing well, thank you!';
|
||||
|
||||
$textResult = new TextResult($assistantContent);
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('load')
|
||||
->willReturn($existingMessages);
|
||||
|
||||
$this->agent->expects($this->once())
|
||||
->method('call')
|
||||
->with($this->callback(function (MessageBag $messages) use ($userMessage) {
|
||||
$messagesArray = $messages->getMessages();
|
||||
|
||||
return end($messagesArray) === $userMessage;
|
||||
}))
|
||||
->willReturn($textResult);
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('save')
|
||||
->with($this->callback(function (MessageBag $messages) use ($userMessage, $assistantContent) {
|
||||
$messagesArray = $messages->getMessages();
|
||||
$lastTwo = \array_slice($messagesArray, -2);
|
||||
|
||||
return 2 === \count($lastTwo)
|
||||
&& $lastTwo[0] === $userMessage
|
||||
&& $lastTwo[1] instanceof AssistantMessage
|
||||
&& $lastTwo[1]->content === $assistantContent;
|
||||
}));
|
||||
|
||||
$result = $this->chat->submit($userMessage);
|
||||
|
||||
$this->assertInstanceOf(AssistantMessage::class, $result);
|
||||
$this->assertSame($assistantContent, $result->content);
|
||||
}
|
||||
|
||||
public function testItAppendsMessagesToExistingConversation()
|
||||
{
|
||||
$existingUserMessage = Message::ofUser('What is the weather?');
|
||||
$existingAssistantMessage = Message::ofAssistant('I cannot provide weather information.');
|
||||
|
||||
$existingMessages = new MessageBag();
|
||||
$existingMessages->add($existingUserMessage);
|
||||
$existingMessages->add($existingAssistantMessage);
|
||||
|
||||
$newUserMessage = Message::ofUser('Can you help with programming?');
|
||||
$newAssistantContent = 'Yes, I can help with programming!';
|
||||
|
||||
$textResult = new TextResult($newAssistantContent);
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('load')
|
||||
->willReturn($existingMessages);
|
||||
|
||||
$this->agent->expects($this->once())
|
||||
->method('call')
|
||||
->with($this->callback(function (MessageBag $messages) {
|
||||
$messagesArray = $messages->getMessages();
|
||||
|
||||
return 3 === \count($messagesArray);
|
||||
}))
|
||||
->willReturn($textResult);
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('save')
|
||||
->with($this->callback(function (MessageBag $messages) {
|
||||
$messagesArray = $messages->getMessages();
|
||||
|
||||
return 4 === \count($messagesArray);
|
||||
}));
|
||||
|
||||
$result = $this->chat->submit($newUserMessage);
|
||||
|
||||
$this->assertInstanceOf(AssistantMessage::class, $result);
|
||||
$this->assertSame($newAssistantContent, $result->content);
|
||||
}
|
||||
|
||||
public function testItHandlesEmptyMessageStore()
|
||||
{
|
||||
$userMessage = Message::ofUser('First message');
|
||||
$emptyMessages = new MessageBag();
|
||||
$assistantContent = 'First response';
|
||||
|
||||
$textResult = new TextResult($assistantContent);
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('load')
|
||||
->willReturn($emptyMessages);
|
||||
|
||||
$this->agent->expects($this->once())
|
||||
->method('call')
|
||||
->with($this->callback(function (MessageBag $messages) {
|
||||
$messagesArray = $messages->getMessages();
|
||||
|
||||
return 1 === \count($messagesArray);
|
||||
}))
|
||||
->willReturn($textResult);
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('save');
|
||||
|
||||
$result = $this->chat->submit($userMessage);
|
||||
|
||||
$this->assertInstanceOf(AssistantMessage::class, $result);
|
||||
$this->assertSame($assistantContent, $result->content);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user