Skip to content

Commit 319f70c

Browse files
committed
Add optional translation to system prompts
So that system prompts can be written in any locale
1 parent d8dfb30 commit 319f70c

File tree

7 files changed

+109
-8
lines changed

7 files changed

+109
-8
lines changed

src/agent/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ CHANGELOG
5454
* Add model capability detection before processing
5555
* Add comprehensive type safety with full PHP type hints
5656
* Add clear exception hierarchy for different error scenarios
57+
* Add translation support for system prompts

src/agent/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"symfony/css-selector": "^6.4 || ^7.1",
4242
"symfony/dom-crawler": "^6.4 || ^7.1",
4343
"symfony/event-dispatcher": "^6.4 || ^7.1",
44-
"symfony/http-foundation": "^6.4 || ^7.1"
44+
"symfony/http-foundation": "^6.4 || ^7.1",
45+
"symfony/translation-contracts": "^3.6"
4546
},
4647
"autoload": {
4748
"psr-4": {

src/agent/src/InputProcessor/SystemPromptInputProcessor.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313

1414
use Psr\Log\LoggerInterface;
1515
use Psr\Log\NullLogger;
16+
use Symfony\AI\Agent\Exception\RuntimeException;
1617
use Symfony\AI\Agent\Input;
1718
use Symfony\AI\Agent\InputProcessorInterface;
1819
use Symfony\AI\Agent\Toolbox\ToolboxInterface;
1920
use Symfony\AI\Platform\Message\Message;
2021
use Symfony\AI\Platform\Tool\Tool;
22+
use Symfony\Contracts\Translation\TranslatorInterface;
2123

2224
/**
2325
* @author Christopher Hertel <mail@christopher-hertel.de>
@@ -31,8 +33,14 @@
3133
public function __construct(
3234
private \Stringable|string $systemPrompt,
3335
private ?ToolboxInterface $toolbox = null,
36+
private ?TranslatorInterface $translator = null,
37+
private bool $enableTranslation = false,
38+
private ?string $translationDomain = null,
3439
private LoggerInterface $logger = new NullLogger(),
3540
) {
41+
if ($this->enableTranslation && !$this->translator) {
42+
throw new RuntimeException('Prompt translation is enabled but no translator was provided.');
43+
}
3644
}
3745

3846
public function processInput(Input $input): void
@@ -45,7 +53,9 @@ public function processInput(Input $input): void
4553
return;
4654
}
4755

48-
$message = (string) $this->systemPrompt;
56+
$message = $this->enableTranslation
57+
? $this->translator->trans((string) $this->systemPrompt, [], $this->translationDomain)
58+
: (string) $this->systemPrompt;
4959

5060
if ($this->toolbox instanceof ToolboxInterface
5161
&& [] !== $this->toolbox->getTools()
@@ -61,7 +71,7 @@ public function processInput(Input $input): void
6171
));
6272

6373
$message = <<<PROMPT
64-
{$this->systemPrompt}
74+
{$message}
6575
6676
# Tools
6777

src/agent/tests/InputProcessor/SystemPromptInputProcessorTest.php

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Symfony\AI\Platform\Result\ToolCall;
3030
use Symfony\AI\Platform\Tool\ExecutionReference;
3131
use Symfony\AI\Platform\Tool\Tool;
32+
use Symfony\Contracts\Translation\TranslatorInterface;
3233

3334
#[CoversClass(SystemPromptInputProcessor::class)]
3435
#[UsesClass(Gpt::class)]
@@ -89,7 +90,7 @@ public function execute(ToolCall $toolCall): mixed
8990
{
9091
return null;
9192
}
92-
}
93+
},
9394
);
9495

9596
$input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')));
@@ -105,7 +106,7 @@ public function execute(ToolCall $toolCall): mixed
105106
public function testIncludeToolDefinitions()
106107
{
107108
$processor = new SystemPromptInputProcessor(
108-
'This is a system prompt',
109+
'This is a',
109110
new class implements ToolboxInterface {
110111
public function getTools(): array
111112
{
@@ -127,7 +128,9 @@ public function execute(ToolCall $toolCall): mixed
127128
{
128129
return null;
129130
}
130-
}
131+
},
132+
$this->getTranslator(),
133+
true,
131134
);
132135

133136
$input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')));
@@ -138,7 +141,7 @@ public function execute(ToolCall $toolCall): mixed
138141
$this->assertInstanceOf(SystemMessage::class, $messages[0]);
139142
$this->assertInstanceOf(UserMessage::class, $messages[1]);
140143
$this->assertSame(<<<PROMPT
141-
This is a system prompt
144+
This is a cool translated system prompt
142145
143146
# Tools
144147
@@ -169,7 +172,7 @@ public function execute(ToolCall $toolCall): mixed
169172
{
170173
return null;
171174
}
172-
}
175+
},
173176
);
174177

175178
$input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')));
@@ -190,4 +193,66 @@ public function execute(ToolCall $toolCall): mixed
190193
A tool without parameters
191194
PROMPT, $messages[0]->content);
192195
}
196+
197+
public function testWithTranslatedSystemPrompt()
198+
{
199+
$processor = new SystemPromptInputProcessor('This is a', null, $this->getTranslator(), true);
200+
201+
$input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')), []);
202+
$processor->processInput($input);
203+
204+
$messages = $input->messages->getMessages();
205+
$this->assertCount(2, $messages);
206+
$this->assertInstanceOf(SystemMessage::class, $messages[0]);
207+
$this->assertInstanceOf(UserMessage::class, $messages[1]);
208+
$this->assertSame('This is a cool translated system prompt', $messages[0]->content);
209+
}
210+
211+
public function testWithTranslationDomainSystemPrompt()
212+
{
213+
$processor = new SystemPromptInputProcessor(
214+
'This is a',
215+
null,
216+
$this->getTranslator(),
217+
true,
218+
'prompts'
219+
);
220+
221+
$input = new Input(new Gpt(), new MessageBag(), []);
222+
$processor->processInput($input);
223+
224+
$messages = $input->messages->getMessages();
225+
$this->assertCount(1, $messages);
226+
$this->assertInstanceOf(SystemMessage::class, $messages[0]);
227+
$this->assertSame('This is a cool translated system prompt with a translation domain', $messages[0]->content);
228+
}
229+
230+
public function testWithMissingTranslator()
231+
{
232+
$this->expectExceptionMessage('Prompt translation is enabled but no translator was provided');
233+
234+
new SystemPromptInputProcessor(
235+
'This is a',
236+
null,
237+
null,
238+
true,
239+
);
240+
}
241+
242+
private function getTranslator(): TranslatorInterface
243+
{
244+
return new class implements TranslatorInterface {
245+
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
246+
{
247+
$translated = \sprintf('%s cool translated system prompt', $id);
248+
249+
return $domain ? $translated.' with a translation domain' : $translated;
250+
}
251+
252+
public function getLocale(): string
253+
{
254+
return 'en';
255+
}
256+
};
257+
}
193258
}

src/ai-bundle/config/options.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\AI\Platform\PlatformInterface;
1919
use Symfony\AI\Store\Document\VectorizerInterface;
2020
use Symfony\AI\Store\StoreInterface;
21+
use Symfony\Contracts\Translation\TranslatorInterface;
2122

2223
return static function (DefinitionConfigurator $configurator): void {
2324
$configurator->rootNode()
@@ -154,6 +155,12 @@
154155
})
155156
->thenInvalid('The "text" cannot be empty.')
156157
->end()
158+
->validate()
159+
->ifTrue(function ($v) {
160+
return \is_array($v) && $v['enabled'] && !interface_exists(TranslatorInterface::class);
161+
})
162+
->thenInvalid('System prompt translation is enabled, but no translator is present. Try running `composer require symfony/translation`.')
163+
->end()
157164
->children()
158165
->scalarNode('text')
159166
->info('The system prompt text')
@@ -162,6 +169,14 @@
162169
->info('Include tool definitions at the end of the system prompt')
163170
->defaultFalse()
164171
->end()
172+
->booleanNode('enable_translation')
173+
->info('Enable translation for the system prompt')
174+
->defaultFalse()
175+
->end()
176+
->scalarNode('translation_domain')
177+
->info('The translation domain for the system prompt')
178+
->defaultNull()
179+
->end()
165180
->end()
166181
->end()
167182
->arrayNode('tools')

src/ai-bundle/src/AiBundle.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,9 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
611611
->setArguments([
612612
$config['prompt']['text'],
613613
$includeTools ? new Reference('ai.toolbox.'.$name) : null,
614+
new Reference('translator', ContainerInterface::NULL_ON_INVALID_REFERENCE),
615+
$config['prompt']['enable_translation'],
616+
$config['prompt']['translation_domain'],
614617
new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
615618
])
616619
->addTag('ai.agent.input_processor', ['agent' => $agentId, 'priority' => -30]);

src/ai-bundle/tests/DependencyInjection/AiBundleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,11 +471,13 @@ public function testMultipleAgentsWithProcessors()
471471
$firstSystemPrompt = $container->getDefinition('ai.agent.first_agent.system_prompt_processor');
472472
$firstSystemTags = $firstSystemPrompt->getTag('ai.agent.input_processor');
473473
$this->assertSame($firstAgentId, $firstSystemTags[0]['agent']);
474+
$this->assertCount(3, array_filter($firstSystemPrompt->getArguments()));
474475

475476
// Second agent system prompt processor
476477
$secondSystemPrompt = $container->getDefinition('ai.agent.second_agent.system_prompt_processor');
477478
$secondSystemTags = $secondSystemPrompt->getTag('ai.agent.input_processor');
478479
$this->assertSame($secondAgentId, $secondSystemTags[0]['agent']);
480+
$this->assertCount(3, array_filter($secondSystemPrompt->getArguments()));
479481
}
480482

481483
#[TestDox('Processors work correctly when using the default toolbox')]
@@ -658,6 +660,8 @@ public function testSystemPromptWithArrayStructure()
658660
'model' => ['class' => 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'],
659661
'prompt' => [
660662
'text' => 'You are a helpful assistant.',
663+
'enable_translation' => true,
664+
'translation_domain' => 'prompts',
661665
],
662666
'tools' => [
663667
['service' => 'some_tool', 'description' => 'Test tool'],
@@ -673,6 +677,8 @@ public function testSystemPromptWithArrayStructure()
673677

674678
$this->assertSame('You are a helpful assistant.', $arguments[0]);
675679
$this->assertNull($arguments[1]); // include_tools is false, so null reference
680+
$this->assertTrue($arguments[3]);
681+
$this->assertSame('prompts', $arguments[4]);
676682
}
677683

678684
#[TestDox('System prompt with include_tools enabled works correctly')]

0 commit comments

Comments
 (0)