From 868581f45e186e385cdca745d81ee6ef58486bfa Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 18 Sep 2025 15:34:47 +0200 Subject: [PATCH] [Agent] Allow translatable prompts in `SystemPromptInputProcessor` --- src/agent/composer.json | 1 + .../SystemPromptInputProcessor.php | 17 ++++++++--------- .../SystemPromptInputProcessorTest.php | 15 ++++++--------- src/ai-bundle/composer.json | 3 ++- src/ai-bundle/src/AiBundle.php | 15 ++++++++++++--- .../tests/DependencyInjection/AiBundleTest.php | 5 ++--- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/agent/composer.json b/src/agent/composer.json index 43d5cb4bb..dded2a257 100644 --- a/src/agent/composer.json +++ b/src/agent/composer.json @@ -42,6 +42,7 @@ "symfony/dom-crawler": "^7.3|^8.0", "symfony/event-dispatcher": "^7.3|^8.0", "symfony/http-foundation": "^7.3|^8.0", + "symfony/translation": "^7.3|^8.0", "symfony/translation-contracts": "^3.6" }, "autoload": { diff --git a/src/agent/src/InputProcessor/SystemPromptInputProcessor.php b/src/agent/src/InputProcessor/SystemPromptInputProcessor.php index c6bd60173..ad7871abb 100644 --- a/src/agent/src/InputProcessor/SystemPromptInputProcessor.php +++ b/src/agent/src/InputProcessor/SystemPromptInputProcessor.php @@ -19,6 +19,7 @@ use Symfony\AI\Agent\Toolbox\ToolboxInterface; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Tool\Tool; +use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -27,19 +28,17 @@ final readonly class SystemPromptInputProcessor implements InputProcessorInterface { /** - * @param \Stringable|string $systemPrompt the system prompt to prepend to the input messages - * @param ToolboxInterface|null $toolbox the tool box to be used to append the tool definitions to the system prompt + * @param \Stringable|TranslatableInterface|string $systemPrompt the system prompt to prepend to the input messages + * @param ToolboxInterface|null $toolbox the tool box to be used to append the tool definitions to the system prompt */ public function __construct( - private \Stringable|string $systemPrompt, + private \Stringable|TranslatableInterface|string $systemPrompt, private ?ToolboxInterface $toolbox = null, private ?TranslatorInterface $translator = null, - private bool $enableTranslation = false, - private ?string $translationDomain = null, private LoggerInterface $logger = new NullLogger(), ) { - if ($this->enableTranslation && !$this->translator) { - throw new RuntimeException('Prompt translation is enabled but no translator was provided.'); + if ($this->systemPrompt instanceof TranslatableInterface && !$this->translator) { + throw new RuntimeException('Translatable system prompt is not supported when no translator is provided.'); } } @@ -53,8 +52,8 @@ public function processInput(Input $input): void return; } - $message = $this->enableTranslation - ? $this->translator->trans((string) $this->systemPrompt, [], $this->translationDomain) + $message = $this->systemPrompt instanceof TranslatableInterface + ? $this->systemPrompt->trans($this->translator) : (string) $this->systemPrompt; if ($this->toolbox instanceof ToolboxInterface diff --git a/src/agent/tests/InputProcessor/SystemPromptInputProcessorTest.php b/src/agent/tests/InputProcessor/SystemPromptInputProcessorTest.php index e0177ff4d..8d3fc9da6 100644 --- a/src/agent/tests/InputProcessor/SystemPromptInputProcessorTest.php +++ b/src/agent/tests/InputProcessor/SystemPromptInputProcessorTest.php @@ -29,6 +29,7 @@ use Symfony\AI\Platform\Result\ToolCall; use Symfony\AI\Platform\Tool\ExecutionReference; use Symfony\AI\Platform\Tool\Tool; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Contracts\Translation\TranslatorInterface; #[CoversClass(SystemPromptInputProcessor::class)] @@ -106,7 +107,7 @@ public function execute(ToolCall $toolCall): mixed public function testIncludeToolDefinitions() { $processor = new SystemPromptInputProcessor( - 'This is a', + new TranslatableMessage('This is a'), new class implements ToolboxInterface { public function getTools(): array { @@ -130,7 +131,6 @@ public function execute(ToolCall $toolCall): mixed } }, $this->getTranslator(), - true, ); $input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message'))); @@ -196,7 +196,7 @@ public function execute(ToolCall $toolCall): mixed public function testWithTranslatedSystemPrompt() { - $processor = new SystemPromptInputProcessor('This is a', null, $this->getTranslator(), true); + $processor = new SystemPromptInputProcessor(new TranslatableMessage('This is a'), null, $this->getTranslator()); $input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')), []); $processor->processInput($input); @@ -211,11 +211,9 @@ public function testWithTranslatedSystemPrompt() public function testWithTranslationDomainSystemPrompt() { $processor = new SystemPromptInputProcessor( - 'This is a', + new TranslatableMessage('This is a', domain: 'prompts'), null, $this->getTranslator(), - true, - 'prompts' ); $input = new Input(new Gpt(), new MessageBag(), []); @@ -229,13 +227,12 @@ public function testWithTranslationDomainSystemPrompt() public function testWithMissingTranslator() { - $this->expectExceptionMessage('Prompt translation is enabled but no translator was provided'); + $this->expectExceptionMessage('Translatable system prompt is not supported when no translator is provided.'); new SystemPromptInputProcessor( - 'This is a', + new TranslatableMessage('This is a'), null, null, - true, ); } diff --git a/src/ai-bundle/composer.json b/src/ai-bundle/composer.json index 9ec551bad..13a855f71 100644 --- a/src/ai-bundle/composer.json +++ b/src/ai-bundle/composer.json @@ -29,7 +29,8 @@ "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^11.5", "symfony/expression-language": "^7.3|^8.0", - "symfony/security-core": "^7.3|^8.0" + "symfony/security-core": "^7.3|^8.0", + "symfony/translation": "^7.3|^8.0" }, "autoload": { "psr-4": { diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index e5f48dc47..5a0decc36 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -84,6 +84,7 @@ use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Contracts\HttpClient\HttpClientInterface; use function Symfony\Component\String\u; @@ -623,13 +624,21 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde if (isset($config['prompt'])) { $includeTools = isset($config['prompt']['include_tools']) && $config['prompt']['include_tools']; + if ($config['prompt']['enable_translation']) { + if (!class_exists(TranslatableMessage::class)) { + throw new RuntimeException('For using prompt translataion, symfony/translation package is required. Try running "composer require symfony/translation".'); + } + + $prompt = new TranslatableMessage($config['prompt']['text'], domain: $config['prompt']['translation_domain']); + } else { + $prompt = $config['prompt']['text']; + } + $systemPromptInputProcessorDefinition = (new Definition(SystemPromptInputProcessor::class)) ->setArguments([ - $config['prompt']['text'], + $prompt, $includeTools ? new Reference('ai.toolbox.'.$name) : null, new Reference('translator', ContainerInterface::NULL_ON_INVALID_REFERENCE), - $config['prompt']['enable_translation'], - $config['prompt']['translation_domain'], 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 9864b80be..54e39d085 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -34,6 +34,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Translation\TranslatableMessage; #[CoversClass(AiBundle::class)] #[UsesClass(ContainerBuilder::class)] @@ -682,10 +683,8 @@ public function testSystemPromptWithArrayStructure() $definition = $container->getDefinition('ai.agent.test_agent.system_prompt_processor'); $arguments = $definition->getArguments(); - $this->assertSame('You are a helpful assistant.', $arguments[0]); + $this->assertEquals(new TranslatableMessage('You are a helpful assistant.', domain: 'prompts'), $arguments[0]); $this->assertNull($arguments[1]); // include_tools is false, so null reference - $this->assertTrue($arguments[3]); - $this->assertSame('prompts', $arguments[4]); } #[TestDox('System prompt with include_tools enabled works correctly')]