Skip to content

Commit 5a2d253

Browse files
OskarStarkclaude
andcommitted
[AI Bundle] Add tests to prevent processor configuration regressions
This commit adds comprehensive tests for processor configuration to ensure that processor tags always use the full agent ID (ai.agent.{name}) instead of just the agent name. This prevents regressions like the one fixed in symfony#428. The tests cover: - Tool processor tags for specific and default toolboxes - Structured output processor tags - System prompt processor tags - Token usage processor tags - Multiple agents with their own processors - Special characters in agent names Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f9be079 commit 5a2d253

File tree

2 files changed

+281
-8
lines changed

2 files changed

+281
-8
lines changed

src/ai-bundle/src/AiBundle.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -516,8 +516,8 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
516516
->replaceArgument(0, new Reference('ai.toolbox.'.$name));
517517

518518
$container->setDefinition('ai.tool.agent_processor.'.$name, $toolProcessorDefinition)
519-
->addTag('ai.agent.input_processor', ['agent' => $name, 'priority' => -10])
520-
->addTag('ai.agent.output_processor', ['agent' => $name, 'priority' => -10]);
519+
->addTag('ai.agent.input_processor', ['agent' => 'ai.agent.'.$name, 'priority' => -10])
520+
->addTag('ai.agent.output_processor', ['agent' => 'ai.agent.'.$name, 'priority' => -10]);
521521
} else {
522522
if ($config['fault_tolerant_toolbox'] && !$container->hasDefinition('ai.fault_tolerant_toolbox')) {
523523
$container->setDefinition('ai.fault_tolerant_toolbox', new Definition(FaultTolerantToolbox::class))
@@ -526,16 +526,16 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
526526
}
527527

528528
$container->getDefinition('ai.tool.agent_processor')
529-
->addTag('ai.agent.input_processor', ['agent' => $name, 'priority' => -10])
530-
->addTag('ai.agent.output_processor', ['agent' => $name, 'priority' => -10]);
529+
->addTag('ai.agent.input_processor', ['agent' => 'ai.agent.'.$name, 'priority' => -10])
530+
->addTag('ai.agent.output_processor', ['agent' => 'ai.agent.'.$name, 'priority' => -10]);
531531
}
532532
}
533533

534534
// STRUCTURED OUTPUT
535535
if ($config['structured_output']) {
536536
$container->getDefinition('ai.agent.structured_output_processor')
537-
->addTag('ai.agent.input_processor', ['agent' => $name, 'priority' => -20])
538-
->addTag('ai.agent.output_processor', ['agent' => $name, 'priority' => -20]);
537+
->addTag('ai.agent.input_processor', ['agent' => 'ai.agent.'.$name, 'priority' => -20])
538+
->addTag('ai.agent.output_processor', ['agent' => 'ai.agent.'.$name, 'priority' => -20]);
539539
}
540540

541541
// TOKEN USAGE TRACKING
@@ -555,7 +555,7 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
555555

556556
if ($container->hasDefinition('ai.platform.token_usage_processor.'.$platform)) {
557557
$container->getDefinition('ai.platform.token_usage_processor.'.$platform)
558-
->addTag('ai.agent.output_processor', ['agent' => $name, 'priority' => -30]);
558+
->addTag('ai.agent.output_processor', ['agent' => 'ai.agent.'.$name, 'priority' => -30]);
559559
}
560560
}
561561
}
@@ -568,7 +568,7 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
568568
$config['include_tools'] ? new Reference('ai.toolbox.'.$name) : null,
569569
new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
570570
])
571-
->addTag('ai.agent.input_processor', ['agent' => $name, 'priority' => -30]);
571+
->addTag('ai.agent.input_processor', ['agent' => 'ai.agent.'.$name, 'priority' => -30]);
572572

573573
$container->setDefinition('ai.agent.'.$name.'.system_prompt_processor', $systemPromptInputProcessorDefinition);
574574
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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\AiBundle\Tests\DependencyInjection;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\DataProvider;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\AI\AiBundle\AiBundle;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
20+
#[CoversClass(AiBundle::class)]
21+
class ProcessorConfigurationTest extends TestCase
22+
{
23+
/**
24+
* Tests that processor tags use the full agent ID (ai.agent.my_agent) instead of just the agent name (my_agent).
25+
* This regression test prevents issues where processors would not be correctly associated with their agents.
26+
*
27+
* @see https://github.com/symfony/ai/pull/428
28+
*/
29+
public function testProcessorTagsUseFullAgentId(): void
30+
{
31+
$container = $this->buildContainer([
32+
'ai' => [
33+
'agent' => [
34+
'test_agent' => [
35+
'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'],
36+
'tools' => [
37+
['service' => 'some_tool', 'description' => 'Test tool'],
38+
],
39+
'structured_output' => true,
40+
'system_prompt' => 'You are a test assistant.',
41+
],
42+
],
43+
],
44+
]);
45+
46+
$agentId = 'ai.agent.test_agent';
47+
48+
// Test tool processor tags
49+
$toolProcessorDefinition = $container->getDefinition('ai.tool.agent_processor.test_agent');
50+
$toolProcessorTags = $toolProcessorDefinition->getTag('ai.agent.input_processor');
51+
$this->assertNotEmpty($toolProcessorTags, 'Tool processor should have input processor tags');
52+
$this->assertSame($agentId, $toolProcessorTags[0]['agent'], 'Tool input processor tag should use full agent ID');
53+
54+
$outputTags = $toolProcessorDefinition->getTag('ai.agent.output_processor');
55+
$this->assertNotEmpty($outputTags, 'Tool processor should have output processor tags');
56+
$this->assertSame($agentId, $outputTags[0]['agent'], 'Tool output processor tag should use full agent ID');
57+
58+
// Test structured output processor tags
59+
$structuredOutputTags = $container->getDefinition('ai.agent.structured_output_processor')
60+
->getTag('ai.agent.input_processor');
61+
$this->assertNotEmpty($structuredOutputTags, 'Structured output processor should have input processor tags');
62+
63+
// Find the tag for our specific agent
64+
$foundAgentTag = false;
65+
foreach ($structuredOutputTags as $tag) {
66+
if (($tag['agent'] ?? '') === $agentId) {
67+
$foundAgentTag = true;
68+
break;
69+
}
70+
}
71+
$this->assertTrue($foundAgentTag, 'Structured output processor should have tag with full agent ID');
72+
73+
// Test system prompt processor tags
74+
$systemPromptDefinition = $container->getDefinition('ai.agent.test_agent.system_prompt_processor');
75+
$systemPromptTags = $systemPromptDefinition->getTag('ai.agent.input_processor');
76+
$this->assertNotEmpty($systemPromptTags, 'System prompt processor should have input processor tags');
77+
$this->assertSame($agentId, $systemPromptTags[0]['agent'], 'System prompt processor tag should use full agent ID');
78+
}
79+
80+
/**
81+
* Tests that processors work correctly with multiple agents.
82+
*/
83+
public function testMultipleAgentsWithProcessors(): void
84+
{
85+
$container = $this->buildContainer([
86+
'ai' => [
87+
'agent' => [
88+
'first_agent' => [
89+
'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'],
90+
'tools' => [
91+
['service' => 'tool_one', 'description' => 'Tool for first agent'],
92+
],
93+
'system_prompt' => 'First agent prompt',
94+
],
95+
'second_agent' => [
96+
'model' => ['class' => 'Symfony\AI\Platform\Bridge\Anthropic\Claude'],
97+
'tools' => [
98+
['service' => 'tool_two', 'description' => 'Tool for second agent'],
99+
],
100+
'system_prompt' => 'Second agent prompt',
101+
],
102+
],
103+
],
104+
]);
105+
106+
// Check that each agent has its own properly tagged processors
107+
$firstAgentId = 'ai.agent.first_agent';
108+
$secondAgentId = 'ai.agent.second_agent';
109+
110+
// First agent tool processor
111+
$firstToolProcessor = $container->getDefinition('ai.tool.agent_processor.first_agent');
112+
$firstToolTags = $firstToolProcessor->getTag('ai.agent.input_processor');
113+
$this->assertSame($firstAgentId, $firstToolTags[0]['agent']);
114+
115+
// Second agent tool processor
116+
$secondToolProcessor = $container->getDefinition('ai.tool.agent_processor.second_agent');
117+
$secondToolTags = $secondToolProcessor->getTag('ai.agent.input_processor');
118+
$this->assertSame($secondAgentId, $secondToolTags[0]['agent']);
119+
120+
// First agent system prompt processor
121+
$firstSystemPrompt = $container->getDefinition('ai.agent.first_agent.system_prompt_processor');
122+
$firstSystemTags = $firstSystemPrompt->getTag('ai.agent.input_processor');
123+
$this->assertSame($firstAgentId, $firstSystemTags[0]['agent']);
124+
125+
// Second agent system prompt processor
126+
$secondSystemPrompt = $container->getDefinition('ai.agent.second_agent.system_prompt_processor');
127+
$secondSystemTags = $secondSystemPrompt->getTag('ai.agent.input_processor');
128+
$this->assertSame($secondAgentId, $secondSystemTags[0]['agent']);
129+
}
130+
131+
/**
132+
* Tests that processors work correctly when using the default toolbox.
133+
*/
134+
public function testDefaultToolboxProcessorTags(): void
135+
{
136+
$container = $this->buildContainer([
137+
'ai' => [
138+
'agent' => [
139+
'agent_with_default_toolbox' => [
140+
'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'],
141+
'tools' => true,
142+
],
143+
],
144+
],
145+
]);
146+
147+
$agentId = 'ai.agent.agent_with_default_toolbox';
148+
149+
// When using default toolbox, the ai.tool.agent_processor service gets the tags
150+
$defaultToolProcessor = $container->getDefinition('ai.tool.agent_processor');
151+
$inputTags = $defaultToolProcessor->getTag('ai.agent.input_processor');
152+
$outputTags = $defaultToolProcessor->getTag('ai.agent.output_processor');
153+
154+
// Find tags for our specific agent
155+
$foundInput = false;
156+
$foundOutput = false;
157+
158+
foreach ($inputTags as $tag) {
159+
if (($tag['agent'] ?? '') === $agentId) {
160+
$foundInput = true;
161+
break;
162+
}
163+
}
164+
165+
foreach ($outputTags as $tag) {
166+
if (($tag['agent'] ?? '') === $agentId) {
167+
$foundOutput = true;
168+
break;
169+
}
170+
}
171+
172+
$this->assertTrue($foundInput, 'Default tool processor should have input tag with full agent ID');
173+
$this->assertTrue($foundOutput, 'Default tool processor should have output tag with full agent ID');
174+
}
175+
176+
/**
177+
* Tests processor tags with special characters in agent names.
178+
*/
179+
#[DataProvider('specialAgentNamesProvider')]
180+
public function testProcessorTagsWithSpecialAgentNames(string $agentName): void
181+
{
182+
$container = $this->buildContainer([
183+
'ai' => [
184+
'agent' => [
185+
$agentName => [
186+
'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'],
187+
'system_prompt' => 'Test prompt',
188+
],
189+
],
190+
],
191+
]);
192+
193+
$expectedAgentId = 'ai.agent.'.$agentName;
194+
195+
// Check if the system prompt processor exists (it won't exist for invalid agent names)
196+
$systemPromptServiceId = 'ai.agent.'.$agentName.'.system_prompt_processor';
197+
if (!$container->hasDefinition($systemPromptServiceId)) {
198+
$this->markTestSkipped(sprintf('Agent name "%s" is not valid for service IDs', $agentName));
199+
}
200+
201+
$systemPromptDefinition = $container->getDefinition($systemPromptServiceId);
202+
$tags = $systemPromptDefinition->getTag('ai.agent.input_processor');
203+
204+
$this->assertNotEmpty($tags);
205+
$this->assertSame($expectedAgentId, $tags[0]['agent'],
206+
sprintf('Processor tag should use full agent ID for agent name "%s"', $agentName));
207+
}
208+
209+
public static function specialAgentNamesProvider(): array
210+
{
211+
return [
212+
'agent with underscore' => ['my_special_agent'],
213+
'agent with dash' => ['my-special-agent'],
214+
'agent with dots' => ['my.special.agent'],
215+
'agent with numbers' => ['agent123'],
216+
'agent with mixed' => ['Agent_v2-final.prod'],
217+
];
218+
}
219+
220+
/**
221+
* Tests that token usage processor tags use the correct agent ID.
222+
*/
223+
public function testTokenUsageProcessorTags(): void
224+
{
225+
$container = $this->buildContainer([
226+
'ai' => [
227+
'platform' => [
228+
'openai' => [
229+
'api_key' => 'test_key',
230+
],
231+
],
232+
'agent' => [
233+
'tracked_agent' => [
234+
'platform' => 'ai.platform.openai',
235+
'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'],
236+
'track_token_usage' => true,
237+
],
238+
],
239+
],
240+
]);
241+
242+
$agentId = 'ai.agent.tracked_agent';
243+
244+
// Check if token usage processor has the correct agent tag
245+
if ($container->hasDefinition('ai.platform.token_usage_processor.openai')) {
246+
$tokenUsageProcessor = $container->getDefinition('ai.platform.token_usage_processor.openai');
247+
$outputTags = $tokenUsageProcessor->getTag('ai.agent.output_processor');
248+
249+
$foundTag = false;
250+
foreach ($outputTags as $tag) {
251+
if (($tag['agent'] ?? '') === $agentId) {
252+
$foundTag = true;
253+
break;
254+
}
255+
}
256+
257+
$this->assertTrue($foundTag, 'Token usage processor should have output tag with full agent ID');
258+
}
259+
}
260+
261+
private function buildContainer(array $configuration): ContainerBuilder
262+
{
263+
$container = new ContainerBuilder();
264+
$container->setParameter('kernel.debug', true);
265+
$container->setParameter('kernel.environment', 'test');
266+
$container->setParameter('kernel.build_dir', 'public');
267+
268+
$extension = (new AiBundle())->getContainerExtension();
269+
$extension->load($configuration, $container);
270+
271+
return $container;
272+
}
273+
}

0 commit comments

Comments
 (0)