From e8b7b22f94227664e0b447c9d19d3cbcb7661bb1 Mon Sep 17 00:00:00 2001 From: valtzu Date: Sun, 20 Jul 2025 22:11:03 +0300 Subject: [PATCH] [AIBundle] Simplify agent-as-tool configuration --- demo/config/packages/ai.yaml | 3 +- src/ai-bundle/config/options.php | 8 ++- src/ai-bundle/doc/index.rst | 3 +- src/ai-bundle/src/AIBundle.php | 6 ++- .../DependencyInjection/AIBundleTest.php | 52 +++++++++++++++++-- 5 files changed, 62 insertions(+), 10 deletions(-) diff --git a/demo/config/packages/ai.yaml b/demo/config/packages/ai.yaml index 12bcc17e..94fd4a1c 100644 --- a/demo/config/packages/ai.yaml +++ b/demo/config/packages/ai.yaml @@ -46,10 +46,9 @@ ai: system_prompt: 'You are a friendly chatbot that likes to have a conversation with users and asks them some questions.' tools: # Agent in agent 🤯 - - service: 'ai.agent.blog' + - agent: 'blog' name: 'symfony_blog' description: 'Can answer questions based on the Symfony blog.' - is_agent: true store: chroma_db: symfonycon: diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 2bbbd9ef..25245f9c 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -117,11 +117,11 @@ ->arrayNode('services') ->arrayPrototype() ->children() - ->scalarNode('service')->isRequired()->end() + ->scalarNode('service')->cannotBeEmpty()->end() + ->scalarNode('agent')->cannotBeEmpty()->end() ->scalarNode('name')->end() ->scalarNode('description')->end() ->scalarNode('method')->end() - ->booleanNode('is_agent')->defaultFalse()->end() ->end() ->beforeNormalization() ->ifString() @@ -129,6 +129,10 @@ return ['service' => $v]; }) ->end() + ->validate() + ->ifTrue(static fn ($v) => !(empty($v['agent']) xor empty($v['service']))) + ->thenInvalid('Either "agent" or "service" must be configured, and never both.') + ->end() ->end() ->end() ->end() diff --git a/src/ai-bundle/doc/index.rst b/src/ai-bundle/doc/index.rst index 7e39d439..63b9db87 100644 --- a/src/ai-bundle/doc/index.rst +++ b/src/ai-bundle/doc/index.rst @@ -72,10 +72,9 @@ Configuration method: 'foo' # Optional with default value '__invoke' # Referencing a agent => agent in agent 🤯 - - service: 'ai.agent.research' + - agent: 'research' name: 'wikipedia_research' description: 'Can research on Wikipedia' - is_agent: true research: platform: 'ai.platform.anthropic' model: diff --git a/src/ai-bundle/src/AIBundle.php b/src/ai-bundle/src/AIBundle.php index 76711b9e..742da94d 100644 --- a/src/ai-bundle/src/AIBundle.php +++ b/src/ai-bundle/src/AIBundle.php @@ -333,10 +333,14 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde $tools = []; foreach ($config['tools']['services'] as $tool) { + if (isset($tool['agent'])) { + $tool['name'] ??= $tool['agent']; + $tool['service'] = \sprintf('ai.agent.%s', $tool['agent']); + } $reference = new Reference($tool['service']); // We use the memory factory in case method, description and name are set if (isset($tool['name'], $tool['description'])) { - if ($tool['is_agent']) { + if (isset($tool['agent'])) { $agentWrapperDefinition = new Definition(AgentTool::class, [$reference]); $container->setDefinition('ai.toolbox.'.$name.'.agent_wrapper.'.$tool['name'], $agentWrapperDefinition); $reference = new Reference('ai.toolbox.'.$name.'.agent_wrapper.'.$tool['name']); diff --git a/src/ai-bundle/tests/DependencyInjection/AIBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AIBundleTest.php index 15b88628..31880414 100644 --- a/src/ai-bundle/tests/DependencyInjection/AIBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AIBundleTest.php @@ -13,9 +13,11 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Symfony\AI\AIBundle\AIBundle; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ContainerBuilder; #[CoversClass(AIBundle::class)] @@ -23,16 +25,60 @@ class AIBundleTest extends TestCase { #[DoesNotPerformAssertions] - public function testExtensionLoad(): void + #[Test] + public function extensionLoadDoesNotThrow(): void + { + $this->buildContainer($this->getFullConfig()); + } + + #[Test] + public function agentsCanBeRegisteredAsTools(): void + { + $container = $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'main_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAI\GPT'], + 'tools' => [ + ['agent' => 'another_agent', 'description' => 'Agent tool with implicit name'], + ['agent' => 'another_agent', 'name' => 'another_agent_instance', 'description' => 'Agent tool with explicit name'], + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.toolbox.main_agent.agent_wrapper.another_agent')); + $this->assertTrue($container->hasDefinition('ai.toolbox.main_agent.agent_wrapper.another_agent_instance')); + } + + #[Test] + public function agentsAsToolsCannotDefineService(): void + { + $this->expectException(InvalidConfigurationException::class); + $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'main_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAI\GPT'], + 'tools' => [['agent' => 'another_agent', 'service' => 'foo_bar', 'description' => 'Agent with service']], + ], + ], + ], + ]); + } + + private function buildContainer(array $configuration): ContainerBuilder { $container = new ContainerBuilder(); $container->setParameter('kernel.debug', true); $container->setParameter('kernel.environment', 'dev'); $container->setParameter('kernel.build_dir', 'public'); + $extension = (new AIBundle())->getContainerExtension(); + $extension->load($configuration, $container); - $configs = $this->getFullConfig(); - $extension->load($configs, $container); + return $container; } /**