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