From 5f786809bd6d1b8c86812f9daf40d3a5c4051445 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Sat, 13 Sep 2025 08:55:25 +0200 Subject: [PATCH] [AI Bundle] Restructure `system_prompt` configuration to nest `include_tools` --- demo/config/packages/ai.yaml | 5 +- src/ai-bundle/config/options.php | 40 +++- src/ai-bundle/doc/index.rst | 42 +++- src/ai-bundle/src/AiBundle.php | 8 +- .../DependencyInjection/AiBundleTest.php | 200 +++++++++++++++++- 5 files changed, 277 insertions(+), 18 deletions(-) diff --git a/demo/config/packages/ai.yaml b/demo/config/packages/ai.yaml index a7928348a..c7123e035 100644 --- a/demo/config/packages/ai.yaml +++ b/demo/config/packages/ai.yaml @@ -35,8 +35,9 @@ ai: name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_4O_MINI options: temperature: 0.5 - system_prompt: 'Please answer the users question based on Wikipedia and provide a link to the article.' - include_tools: true + system_prompt: + prompt: 'Please answer the users question based on Wikipedia and provide a link to the article.' + include_tools: true tools: - 'Symfony\AI\Agent\Toolbox\Tool\Wikipedia' audio: diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index ca8a2d1f4..dd0f83bab 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -130,17 +130,39 @@ ->end() ->end() ->booleanNode('structured_output')->defaultTrue()->end() - ->scalarNode('system_prompt') + ->arrayNode('system_prompt') + ->info('The system prompt configuration') + ->beforeNormalization() + ->ifString() + ->then(function (string $v) { + return ['prompt' => $v]; + }) + ->end() + ->beforeNormalization() + ->ifArray() + ->then(function (array $v) { + if (!isset($v['prompt']) && !isset($v['include_tools'])) { + throw new \InvalidArgumentException('Either "prompt" must be configured for system_prompt.'); + } + + return $v; + }) + ->end() ->validate() - ->ifTrue(fn ($v) => null !== $v && '' === trim($v)) - ->thenInvalid('The default system prompt must not be an empty string') + ->ifTrue(function ($v) { + return \is_array($v) && '' === trim($v['prompt'] ?? ''); + }) + ->thenInvalid('The "prompt" cannot be empty.') + ->end() + ->children() + ->scalarNode('prompt') + ->info('The system prompt text') + ->end() + ->booleanNode('include_tools') + ->info('Include tool definitions at the end of the system prompt') + ->defaultFalse() + ->end() ->end() - ->defaultNull() - ->info('The default system prompt of the agent') - ->end() - ->booleanNode('include_tools') - ->info('Include tool definitions at the end of the system prompt') - ->defaultFalse() ->end() ->arrayNode('tools') ->addDefaultsIfNotSet() diff --git a/src/ai-bundle/doc/index.rst b/src/ai-bundle/doc/index.rst index 101db0af4..cb89d4e65 100644 --- a/src/ai-bundle/doc/index.rst +++ b/src/ai-bundle/doc/index.rst @@ -71,8 +71,9 @@ Configuration model: class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt' name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_4O_MINI - system_prompt: 'You are a helpful assistant that can answer questions.' # The default system prompt of the agent - include_tools: true # Include tool definitions at the end of the system prompt + system_prompt: # The system prompt configuration + prompt: 'You are a helpful assistant that can answer questions.' # The prompt text + include_tools: true # Include tool definitions at the end of the system prompt tools: # Referencing a service with #[AsTool] attribute - 'Symfony\AI\Agent\Toolbox\Tool\SimilaritySearch' @@ -144,6 +145,43 @@ Configuration vectorizer: 'ai.vectorizer.mistral_embeddings' store: 'ai.store.memory.research' +System Prompt Configuration +--------------------------- + +For basic usage, specify the system prompt as a simple string: + +.. code-block:: yaml + + ai: + agent: + my_agent: + model: + class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt' + name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_4O_MINI + system_prompt: 'You are a helpful assistant.' + +**Advanced Configuration** + +For more control, such as including tool definitions in the system prompt, use the array format: + +.. code-block:: yaml + + ai: + agent: + my_agent: + model: + class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt' + name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_4O_MINI + system_prompt: + prompt: 'You are a helpful assistant that can answer questions.' + include_tools: true # Include tool definitions at the end of the system prompt + +The array format supports these options: + +* ``prompt`` (string, required): The system prompt text that will be sent to the AI model +* ``include_tools`` (boolean, optional): When set to ``true``, tool definitions will be appended to the system prompt + + Usage ----- diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index c12033d6d..4e6a431dc 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -604,11 +604,13 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde } // SYSTEM PROMPT - if (\is_string($config['system_prompt'])) { + if (isset($config['system_prompt'])) { + $includeTools = isset($config['system_prompt']['include_tools']) && $config['system_prompt']['include_tools']; + $systemPromptInputProcessorDefinition = (new Definition(SystemPromptInputProcessor::class)) ->setArguments([ - $config['system_prompt'], - $config['include_tools'] ? new Reference('ai.toolbox.'.$name) : null, + $config['system_prompt']['prompt'], + $includeTools ? new Reference('ai.toolbox.'.$name) : null, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), ]) ->addTag('ai.agent.input_processor', ['agent' => $agentId, 'priority' => -30]); diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 9eea6717e..0a967aa5f 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -648,6 +648,200 @@ public function testPerplexityPlatformConfiguration() $this->assertSame('ai.platform.contract.perplexity', (string) $arguments[2]); } + #[TestDox('System prompt with array structure works correctly')] + public function testSystemPromptWithArrayStructure() + { + $container = $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'test_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'], + 'system_prompt' => [ + 'prompt' => 'You are a helpful assistant.', + ], + 'tools' => [ + ['service' => 'some_tool', 'description' => 'Test tool'], + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.agent.test_agent.system_prompt_processor')); + $definition = $container->getDefinition('ai.agent.test_agent.system_prompt_processor'); + $arguments = $definition->getArguments(); + + $this->assertSame('You are a helpful assistant.', $arguments[0]); + $this->assertNull($arguments[1]); // include_tools is false, so null reference + } + + #[TestDox('System prompt with include_tools enabled works correctly')] + public function testSystemPromptWithIncludeToolsEnabled() + { + $container = $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'test_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'], + 'system_prompt' => [ + 'prompt' => 'You are a helpful assistant.', + 'include_tools' => true, + ], + 'tools' => [ + ['service' => 'some_tool', 'description' => 'Test tool'], + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.agent.test_agent.system_prompt_processor')); + $definition = $container->getDefinition('ai.agent.test_agent.system_prompt_processor'); + $arguments = $definition->getArguments(); + + $this->assertSame('You are a helpful assistant.', $arguments[0]); + $this->assertInstanceOf(Reference::class, $arguments[1]); + $this->assertSame('ai.toolbox.test_agent', (string) $arguments[1]); + } + + #[TestDox('System prompt with only prompt key defaults include_tools to false')] + public function testSystemPromptWithOnlyPromptKey() + { + $container = $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'test_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'], + 'system_prompt' => [ + 'prompt' => 'You are a helpful assistant.', + ], + 'tools' => [ + ['service' => 'some_tool', 'description' => 'Test tool'], + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.agent.test_agent.system_prompt_processor')); + $definition = $container->getDefinition('ai.agent.test_agent.system_prompt_processor'); + $arguments = $definition->getArguments(); + + $this->assertSame('You are a helpful assistant.', $arguments[0]); + $this->assertNull($arguments[1]); // include_tools defaults to false + } + + #[TestDox('Agent without system prompt does not create processor')] + public function testAgentWithoutSystemPrompt() + { + $container = $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'test_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'], + ], + ], + ], + ]); + + $this->assertFalse($container->hasDefinition('ai.agent.test_agent.system_prompt_processor')); + } + + #[TestDox('Valid system prompt creates processor correctly')] + public function testValidSystemPromptCreatesProcessor() + { + // This test verifies that valid system prompts work correctly with new structure + $container = $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'test_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'], + 'system_prompt' => [ + 'prompt' => 'Valid prompt', + 'include_tools' => true, + ], + 'tools' => [ + ['service' => 'some_tool', 'description' => 'Test tool'], + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.agent.test_agent.system_prompt_processor')); + $definition = $container->getDefinition('ai.agent.test_agent.system_prompt_processor'); + $arguments = $definition->getArguments(); + + $this->assertSame('Valid prompt', $arguments[0]); + $this->assertInstanceOf(Reference::class, $arguments[1]); + $this->assertSame('ai.toolbox.test_agent', (string) $arguments[1]); + } + + #[TestDox('Empty prompt in array structure throws configuration exception')] + public function testEmptyPromptInArrayThrowsException() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The "prompt" cannot be empty.'); + + $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'test_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'], + 'system_prompt' => [ + 'prompt' => '', + ], + ], + ], + ], + ]); + } + + #[TestDox('System prompt array without prompt key throws configuration exception')] + public function testSystemPromptArrayWithoutPromptKeyThrowsException() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The "prompt" cannot be empty.'); + + $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'test_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'], + 'system_prompt' => [ + 'include_tools' => true, + ], + ], + ], + ], + ]); + } + + #[TestDox('System prompt with string format works correctly')] + public function testSystemPromptWithStringFormat() + { + $container = $this->buildContainer([ + 'ai' => [ + 'agent' => [ + 'test_agent' => [ + 'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'], + 'system_prompt' => 'You are a helpful assistant.', + 'tools' => [ + ['service' => 'some_tool', 'description' => 'Test tool'], + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.agent.test_agent.system_prompt_processor')); + $definition = $container->getDefinition('ai.agent.test_agent.system_prompt_processor'); + $arguments = $definition->getArguments(); + + $this->assertSame('You are a helpful assistant.', $arguments[0]); + $this->assertNull($arguments[1]); // include_tools not enabled with string format + } + public function testVectorizerConfiguration() { $container = $this->buildContainer([ @@ -1076,8 +1270,10 @@ private function getFullConfig(): array ], 'structured_output' => false, 'track_token_usage' => true, - 'system_prompt' => 'You are a helpful assistant.', - 'include_tools' => true, + 'system_prompt' => [ + 'prompt' => 'You are a helpful assistant.', + 'include_tools' => true, + ], 'tools' => [ 'enabled' => true, 'services' => [