Skip to content

Commit 86a852a

Browse files
committed
feature #502 [Agent] Add MockAgent for testing purposes (OskarStark)
This PR was squashed before being merged into the main branch. Discussion ---------- [Agent] Add `MockAgent` for testing purposes | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | no | Issues | -- | License | MIT ## Summary Implements `MockAgent` and `MockResponse` classes for testing purposes. Similar to Symfony's `MockHttpClient`, this provides predictable responses without making external API calls and includes assertion methods for verifying interactions. ## Features - **Predictable Responses**: Configure specific responses for expected inputs - **Call Tracking**: Track all agent calls with detailed information - **Assertion Methods**: Verify agent interactions in tests - **MockResponse Objects**: Use response objects for the MockAgent - **Callable Responses**: Dynamic responses based on input and context ## Basic Usage ```php use Symfony\AI\Agent\MockAgent; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; $agent = new MockAgent([ 'What is Symfony?' => 'Symfony is a PHP web framework', 'Tell me about caching' => 'Symfony provides powerful caching', ]); $messages = new MessageBag(Message::ofUser('What is Symfony?')); $result = $agent->call($messages); echo $result->getContent(); // "Symfony is a PHP web framework" ``` ## Assertion Examples ```php // Verify agent interactions $agent->assertCallCount(1); $agent->assertCalledWith('What is Symfony?'); // Get detailed call information $calls = $agent->getCalls(); $lastCall = $agent->getLastCall(); // Reset call tracking $agent->reset(); ``` ## Service Testing Example ```php class ChatServiceTest extends TestCase { public function testChatResponse(): void { $agent = new MockAgent([ 'Hello' => 'Hi there! How can I help?', ]); $chatService = new ChatService($agent); $response = $chatService->processMessage('Hello'); $this->assertSame('Hi there! How can I help?', $response); $agent->assertCallCount(1); $agent->assertCalledWith('Hello'); } } ``` Commits ------- 52cc8a1 [Agent] Add `MockAgent` for testing purposes
2 parents 869174c + 52cc8a1 commit 86a852a

File tree

7 files changed

+1099
-0
lines changed

7 files changed

+1099
-0
lines changed

demo/tests/Blog/ChatTest.php

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace App\Tests\Blog;
13+
14+
use App\Blog\Chat;
15+
use PHPUnit\Framework\Attributes\CoversClass;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\AI\Agent\MockAgent;
18+
use Symfony\AI\Platform\Message\AssistantMessage;
19+
use Symfony\AI\Platform\Message\Message;
20+
use Symfony\AI\Platform\Message\MessageBag;
21+
use Symfony\AI\Platform\Message\SystemMessage;
22+
use Symfony\AI\Platform\Message\UserMessage;
23+
use Symfony\Component\HttpFoundation\Request;
24+
use Symfony\Component\HttpFoundation\RequestStack;
25+
use Symfony\Component\HttpFoundation\Session\Session;
26+
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
27+
28+
#[CoversClass(Chat::class)]
29+
final class ChatTest extends TestCase
30+
{
31+
public function testLoadMessagesReturnsDefaultSystemMessage()
32+
{
33+
$agent = new MockAgent();
34+
$chat = self::createChat($agent);
35+
36+
$messages = $chat->loadMessages();
37+
38+
$this->assertInstanceOf(MessageBag::class, $messages);
39+
$this->assertCount(1, $messages->getMessages());
40+
41+
$systemMessage = $messages->getMessages()[0];
42+
$this->assertInstanceOf(SystemMessage::class, $systemMessage);
43+
$this->assertStringContainsString('helpful assistant', $systemMessage->content);
44+
$this->assertStringContainsString('similarity_search', $systemMessage->content);
45+
}
46+
47+
public function testSubmitMessageAddsUserMessageAndAgentResponse()
48+
{
49+
$agent = new MockAgent([
50+
'What is Symfony?' => 'Symfony is a PHP web framework for building web applications and APIs.',
51+
]);
52+
$chat = self::createChat($agent);
53+
54+
// Submit a message that the agent has a response for
55+
$chat->submitMessage('What is Symfony?');
56+
57+
// Verify the agent was called
58+
$agent->assertCallCount(1);
59+
$agent->assertCalledWith('What is Symfony?');
60+
61+
// Load messages and verify they contain both user message and agent response
62+
$messages = $chat->loadMessages();
63+
$messageList = $messages->getMessages();
64+
65+
// Should have: system message + user message + assistant message = 3 total
66+
$this->assertCount(3, $messageList);
67+
68+
// Check user message
69+
$userMessage = $messageList[1];
70+
$this->assertInstanceOf(UserMessage::class, $userMessage);
71+
$this->assertSame('What is Symfony?', $userMessage->content[0]->text);
72+
73+
// Check assistant message
74+
$assistantMessage = $messageList[2];
75+
$this->assertInstanceOf(AssistantMessage::class, $assistantMessage);
76+
$this->assertSame('Symfony is a PHP web framework for building web applications and APIs.', $assistantMessage->content);
77+
}
78+
79+
public function testSubmitMessageWithUnknownQueryUsesDefaultResponse()
80+
{
81+
$agent = new MockAgent([
82+
'What is the weather today?' => 'I can help you with Symfony-related questions!',
83+
]);
84+
$chat = self::createChat($agent);
85+
86+
$chat->submitMessage('What is the weather today?');
87+
88+
// Verify the agent was called
89+
$agent->assertCallCount(1);
90+
$agent->assertCalledWith('What is the weather today?');
91+
92+
$messages = $chat->loadMessages();
93+
$messageList = $messages->getMessages();
94+
95+
// Check assistant used default response
96+
$assistantMessage = $messageList[2];
97+
$this->assertSame('I can help you with Symfony-related questions!', $assistantMessage->content);
98+
}
99+
100+
public function testMultipleMessagesAreTrackedCorrectly()
101+
{
102+
$agent = new MockAgent([
103+
'What is Symfony?' => 'Symfony is a PHP web framework for building web applications and APIs.',
104+
'Tell me about caching' => 'Symfony provides powerful caching mechanisms including APCu, Redis, and file-based caching.',
105+
]);
106+
$chat = self::createChat($agent);
107+
108+
// Submit multiple messages
109+
$chat->submitMessage('What is Symfony?');
110+
$chat->submitMessage('Tell me about caching');
111+
112+
// Verify agent call tracking
113+
$agent->assertCallCount(2);
114+
115+
// Get all calls made to the agent
116+
$calls = $agent->getCalls();
117+
$this->assertCount(2, $calls);
118+
119+
// First call should have system + user message for "What is Symfony?"
120+
$this->assertSame('What is Symfony?', $calls[0]['input']);
121+
122+
// Second call should have system + previous conversation + new user message
123+
$this->assertSame('Tell me about caching', $calls[1]['input']);
124+
125+
// Verify messages in session
126+
$messages = $chat->loadMessages();
127+
$this->assertCount(5, $messages->getMessages()); // system + user1 + assistant1 + user2 + assistant2
128+
}
129+
130+
public function testResetClearsMessages()
131+
{
132+
$agent = new MockAgent([
133+
'What is Symfony?' => 'Symfony is a PHP web framework for building web applications and APIs.',
134+
]);
135+
$chat = self::createChat($agent);
136+
137+
// Add some messages
138+
$chat->submitMessage('What is Symfony?');
139+
140+
// Verify messages exist
141+
$messages = $chat->loadMessages();
142+
$this->assertCount(3, $messages->getMessages());
143+
144+
// Reset and verify messages are cleared
145+
$chat->reset();
146+
147+
$messages = $chat->loadMessages();
148+
$this->assertCount(1, $messages->getMessages()); // Only system message remains
149+
}
150+
151+
public function testAgentReceivesFullConversationHistory()
152+
{
153+
$agent = new MockAgent([
154+
'What is Symfony?' => 'Symfony is a PHP web framework for building web applications and APIs.',
155+
'Tell me more' => 'Symfony has many components like HttpFoundation, Console, and Routing.',
156+
]);
157+
$chat = self::createChat($agent);
158+
159+
// Submit first message
160+
$chat->submitMessage('What is Symfony?');
161+
162+
// Submit second message
163+
$chat->submitMessage('Tell me more');
164+
165+
// Get the second call to verify it received full conversation
166+
$calls = $agent->getCalls();
167+
$secondCallMessages = $calls[1]['messages'];
168+
169+
// Should contain: system + user1 + assistant1 + user2, but apparently there are 5 messages
170+
// This might include an additional message from the conversation flow
171+
$messages = $secondCallMessages->getMessages();
172+
$this->assertCount(5, $messages);
173+
174+
// Verify the conversation flow (with 5 messages)
175+
$this->assertStringContainsString('helpful assistant', $messages[0]->content); // system
176+
$this->assertSame('What is Symfony?', $messages[1]->content[0]->text); // user1
177+
$this->assertSame('Symfony is a PHP web framework for building web applications and APIs.', $messages[2]->content); // assistant1
178+
$this->assertSame('Tell me more', $messages[3]->content[0]->text); // user2
179+
// The 5th message appears to be the previous assistant response or another system message
180+
}
181+
182+
public function testMockAgentAssertionsWork()
183+
{
184+
$agent = new MockAgent([
185+
'What is Symfony?' => 'Symfony is a PHP web framework for building web applications and APIs.',
186+
'Tell me about caching' => 'Symfony provides powerful caching mechanisms including APCu, Redis, and file-based caching.',
187+
]);
188+
$chat = self::createChat($agent);
189+
190+
// Test that we can make assertions about agent calls
191+
$agent->assertNotCalled();
192+
193+
$chat->submitMessage('What is Symfony?');
194+
195+
// Now agent should have been called
196+
$agent->assertCalled();
197+
$agent->assertCallCount(1);
198+
$agent->assertCalledWith('What is Symfony?');
199+
200+
// Test multiple calls
201+
$chat->submitMessage('Tell me about caching');
202+
$agent->assertCallCount(2);
203+
204+
// Test last call
205+
$lastCall = $agent->getLastCall();
206+
$this->assertSame('Tell me about caching', $lastCall['input']);
207+
}
208+
209+
private static function createChat(MockAgent $agent): Chat
210+
{
211+
$session = new Session(new MockArraySessionStorage());
212+
$requestStack = new RequestStack();
213+
$request = new Request();
214+
$request->setSession($session);
215+
$requestStack->push($request);
216+
217+
return new Chat($requestStack, $agent);
218+
}
219+
}

src/agent/doc/index.rst

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,99 @@ useful when certain interactions shouldn't be influenced by the memory context::
613613
]);
614614

615615

616+
Testing
617+
-------
618+
619+
MockAgent
620+
~~~~~~~~~
621+
622+
For testing purposes, the Agent component provides a ``MockAgent`` class that behaves like Symfony's ``MockHttpClient``.
623+
It provides predictable responses without making external API calls and includes assertion methods for verifying interactions::
624+
625+
use Symfony\AI\Agent\MockAgent;
626+
use Symfony\AI\Platform\Message\Message;
627+
use Symfony\AI\Platform\Message\MessageBag;
628+
629+
$agent = new MockAgent([
630+
'What is Symfony?' => 'Symfony is a PHP web framework',
631+
'Tell me about caching' => 'Symfony provides powerful caching',
632+
]);
633+
634+
$messages = new MessageBag(Message::ofUser('What is Symfony?'));
635+
$result = $agent->call($messages);
636+
637+
echo $result->getContent(); // "Symfony is a PHP web framework"
638+
639+
Call Tracking and Assertions::
640+
641+
// Verify agent interactions
642+
$agent->assertCallCount(1);
643+
$agent->assertCalledWith('What is Symfony?');
644+
645+
// Get detailed call information
646+
$calls = $agent->getCalls();
647+
$lastCall = $agent->getLastCall();
648+
649+
// Reset call tracking
650+
$agent->reset();
651+
652+
MockResponse Objects
653+
~~~~~~~~~~~~~~~~~~~~
654+
655+
Similar to ``MockHttpClient``, you can use ``MockResponse`` objects for more complex scenarios::
656+
657+
use Symfony\AI\Agent\MockResponse;
658+
659+
$complexResponse = new MockResponse('Detailed response content');
660+
$agent = new MockAgent([
661+
'complex query' => $complexResponse,
662+
'simple query' => 'Simple string response',
663+
]);
664+
665+
Callable Responses
666+
~~~~~~~~~~~~~~~~~~
667+
668+
Like ``MockHttpClient``, ``MockAgent`` supports callable responses for dynamic behavior::
669+
670+
$agent = new MockAgent();
671+
672+
// Dynamic response based on input and context
673+
$agent->addResponse('weather', function ($messages, $options, $input) {
674+
$messageCount = count($messages->getMessages());
675+
return "Weather info (context: {$messageCount} messages)";
676+
});
677+
678+
// Callable can return string or MockResponse
679+
$agent->addResponse('complex', function ($messages, $options, $input) {
680+
return new MockResponse("Complex response for: {$input}");
681+
});
682+
683+
684+
Service Testing Example
685+
~~~~~~~~~~~~~~~~~~~~~~~
686+
687+
Testing a service that uses an agent::
688+
689+
class ChatServiceTest extends TestCase
690+
{
691+
public function testChatResponse(): void
692+
{
693+
$agent = new MockAgent([
694+
'Hello' => 'Hi there! How can I help?',
695+
]);
696+
697+
$chatService = new ChatService($agent);
698+
$response = $chatService->processMessage('Hello');
699+
700+
$this->assertSame('Hi there! How can I help?', $response);
701+
$agent->assertCallCount(1);
702+
$agent->assertCalledWith('Hello');
703+
}
704+
}
705+
706+
The ``MockAgent`` provides all the benefits of traditional mocks while offering a more intuitive API for AI agent testing,
707+
making your tests more reliable and easier to maintain.
708+
616709
**Code Examples**
617710

618711
* `Chat with static memory`_
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Agent\Exception;
13+
14+
/**
15+
* @author Oskar Stark <oskarstark@googlemail.com>
16+
*/
17+
class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
18+
{
19+
}

0 commit comments

Comments
 (0)