refactor(core): chat sub-component started

This commit is contained in:
Guillaume Loulier
2025-09-25 19:46:33 +02:00
parent 12ab829717
commit ddfe12144f
8 changed files with 1 additions and 416 deletions

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}