Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ CHANGELOG
* Add model capability detection before processing
* Add comprehensive type safety with full PHP type hints
* Add clear exception hierarchy for different error scenarios
* Add translation support for system prompts
3 changes: 2 additions & 1 deletion src/agent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"symfony/css-selector": "^6.4 || ^7.1",
"symfony/dom-crawler": "^6.4 || ^7.1",
"symfony/event-dispatcher": "^6.4 || ^7.1",
"symfony/http-foundation": "^6.4 || ^7.1"
"symfony/http-foundation": "^6.4 || ^7.1",
"symfony/translation-contracts": "^3.6"
},
"autoload": {
"psr-4": {
Expand Down
14 changes: 12 additions & 2 deletions src/agent/src/InputProcessor/SystemPromptInputProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\AI\Agent\Exception\RuntimeException;
use Symfony\AI\Agent\Input;
use Symfony\AI\Agent\InputProcessorInterface;
use Symfony\AI\Agent\Toolbox\ToolboxInterface;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Tool\Tool;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
* @author Christopher Hertel <mail@christopher-hertel.de>
Expand All @@ -31,8 +33,14 @@
public function __construct(
private \Stringable|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.');
}
}

public function processInput(Input $input): void
Expand All @@ -45,7 +53,9 @@ public function processInput(Input $input): void
return;
}

$message = (string) $this->systemPrompt;
$message = $this->enableTranslation
? $this->translator->trans((string) $this->systemPrompt, [], $this->translationDomain)
: (string) $this->systemPrompt;

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

$message = <<<PROMPT
{$this->systemPrompt}
{$message}

# Tools

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Symfony\AI\Platform\Result\ToolCall;
use Symfony\AI\Platform\Tool\ExecutionReference;
use Symfony\AI\Platform\Tool\Tool;
use Symfony\Contracts\Translation\TranslatorInterface;

#[CoversClass(SystemPromptInputProcessor::class)]
#[UsesClass(Gpt::class)]
Expand Down Expand Up @@ -89,7 +90,7 @@ public function execute(ToolCall $toolCall): mixed
{
return null;
}
}
},
);

$input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')));
Expand All @@ -105,7 +106,7 @@ public function execute(ToolCall $toolCall): mixed
public function testIncludeToolDefinitions()
{
$processor = new SystemPromptInputProcessor(
'This is a system prompt',
'This is a',
new class implements ToolboxInterface {
public function getTools(): array
{
Expand All @@ -127,7 +128,9 @@ public function execute(ToolCall $toolCall): mixed
{
return null;
}
}
},
$this->getTranslator(),
true,
);

$input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')));
Expand All @@ -138,7 +141,7 @@ public function execute(ToolCall $toolCall): mixed
$this->assertInstanceOf(SystemMessage::class, $messages[0]);
$this->assertInstanceOf(UserMessage::class, $messages[1]);
$this->assertSame(<<<PROMPT
This is a system prompt
This is a cool translated system prompt

# Tools

Expand Down Expand Up @@ -169,7 +172,7 @@ public function execute(ToolCall $toolCall): mixed
{
return null;
}
}
},
);

$input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')));
Expand All @@ -190,4 +193,66 @@ public function execute(ToolCall $toolCall): mixed
A tool without parameters
PROMPT, $messages[0]->content);
}

public function testWithTranslatedSystemPrompt()
{
$processor = new SystemPromptInputProcessor('This is a', null, $this->getTranslator(), true);

$input = new Input(new Gpt(), new MessageBag(Message::ofUser('This is a user message')), []);
$processor->processInput($input);

$messages = $input->messages->getMessages();
$this->assertCount(2, $messages);
$this->assertInstanceOf(SystemMessage::class, $messages[0]);
$this->assertInstanceOf(UserMessage::class, $messages[1]);
$this->assertSame('This is a cool translated system prompt', $messages[0]->content);
}

public function testWithTranslationDomainSystemPrompt()
{
$processor = new SystemPromptInputProcessor(
'This is a',
null,
$this->getTranslator(),
true,
'prompts'
);

$input = new Input(new Gpt(), new MessageBag(), []);
$processor->processInput($input);

$messages = $input->messages->getMessages();
$this->assertCount(1, $messages);
$this->assertInstanceOf(SystemMessage::class, $messages[0]);
$this->assertSame('This is a cool translated system prompt with a translation domain', $messages[0]->content);
}

public function testWithMissingTranslator()
{
$this->expectExceptionMessage('Prompt translation is enabled but no translator was provided');

new SystemPromptInputProcessor(
'This is a',
null,
null,
true,
);
}

private function getTranslator(): TranslatorInterface
{
return new class implements TranslatorInterface {
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
{
$translated = \sprintf('%s cool translated system prompt', $id);

return $domain ? $translated.' with a translation domain' : $translated;
}

public function getLocale(): string
{
return 'en';
}
};
}
}
15 changes: 15 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\AI\Platform\PlatformInterface;
use Symfony\AI\Store\Document\VectorizerInterface;
use Symfony\AI\Store\StoreInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

return static function (DefinitionConfigurator $configurator): void {
$configurator->rootNode()
Expand Down Expand Up @@ -154,6 +155,12 @@
})
->thenInvalid('The "text" cannot be empty.')
->end()
->validate()
->ifTrue(function ($v) {
return \is_array($v) && $v['enabled'] && !interface_exists(TranslatorInterface::class);
})
->thenInvalid('System prompt translation is enabled, but no translator is present. Try running `composer require symfony/translation`.')
->end()
->children()
->scalarNode('text')
->info('The system prompt text')
Expand All @@ -162,6 +169,14 @@
->info('Include tool definitions at the end of the system prompt')
->defaultFalse()
->end()
->booleanNode('enable_translation')
->info('Enable translation for the system prompt')
->defaultFalse()
->end()
->scalarNode('translation_domain')
->info('The translation domain for the system prompt')
->defaultNull()
->end()
->end()
->end()
->arrayNode('tools')
Expand Down
3 changes: 3 additions & 0 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
->setArguments([
$config['prompt']['text'],
$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]);
Expand Down
6 changes: 6 additions & 0 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -471,11 +471,13 @@ public function testMultipleAgentsWithProcessors()
$firstSystemPrompt = $container->getDefinition('ai.agent.first_agent.system_prompt_processor');
$firstSystemTags = $firstSystemPrompt->getTag('ai.agent.input_processor');
$this->assertSame($firstAgentId, $firstSystemTags[0]['agent']);
$this->assertCount(3, array_filter($firstSystemPrompt->getArguments()));

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

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

$this->assertSame('You are a helpful assistant.', $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')]
Expand Down