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
7 changes: 2 additions & 5 deletions demo/config/packages/ai.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@ ai:
method: 'now'
stream:
model: 'gpt-4o-mini'
prompt: |
You are an example chat application where messages from the LLM are streamed to the user using
Server-Sent Events via `symfony/ux-turbo` / Turbo Streams. This example does not use any custom
javascript and solely relies on the built-in `live` & `turbo_stream` Stimulus controllers.
Whatever the user asks, tell them about the application & used technologies.
prompt:
file: '%kernel.project_dir%/prompts/stream-chat.txt'
tools: false
youtube:
model: 'gpt-4o-mini'
Expand Down
4 changes: 4 additions & 0 deletions demo/prompts/stream-chat.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
You are an example chat application where messages from the LLM are streamed to the user using
Server-Sent Events via `symfony/ux-turbo` / Turbo Streams. This example does not use any custom
javascript and solely relies on the built-in `live` & `turbo_stream` Stimulus controllers.
Whatever the user asks, tell them about the application & used technologies.
31 changes: 31 additions & 0 deletions examples/misc/prompt-json-file.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Symfony\AI\Agent\Agent;
use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor;
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
use Symfony\AI\Platform\Message\Content\File;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

require_once dirname(__DIR__).'/bootstrap.php';

$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY'], http_client());

// Load system prompt from a JSON file
$promptFile = File::fromFile(dirname(__DIR__, 2).'/fixtures/prompts/code-reviewer.json');
$systemPromptProcessor = new SystemPromptInputProcessor($promptFile);

$agent = new Agent($platform, 'gpt-4o-mini', [$systemPromptProcessor], logger: logger());
$messages = new MessageBag(Message::ofUser('Review this code: function add($a, $b) { return $a + $b; }'));
$result = $agent->call($messages);

echo $result->getContent().\PHP_EOL;
31 changes: 31 additions & 0 deletions examples/misc/prompt-text-file.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Symfony\AI\Agent\Agent;
use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor;
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
use Symfony\AI\Platform\Message\Content\File;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

require_once dirname(__DIR__).'/bootstrap.php';

$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY'], http_client());

// Load system prompt from a plain text file (.txt)
$promptFile = File::fromFile(dirname(__DIR__, 2).'/fixtures/prompts/helpful-assistant.txt');
$systemPromptProcessor = new SystemPromptInputProcessor($promptFile);

$agent = new Agent($platform, 'gpt-4o-mini', [$systemPromptProcessor], logger: logger());
$messages = new MessageBag(Message::ofUser('Can you explain what dependency injection is?'));
$result = $agent->call($messages);

echo $result->getContent().\PHP_EOL;
12 changes: 12 additions & 0 deletions fixtures/prompts/code-reviewer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"role": "You are an expert code reviewer with deep knowledge of software engineering best practices, design patterns, and code quality.",
"responsibilities": [
"Review code for bugs and potential issues",
"Suggest improvements for code quality and maintainability",
"Identify security vulnerabilities",
"Recommend better design patterns when appropriate",
"Ensure code follows language-specific best practices"
],
"tone": "constructive and educational",
"approach": "Provide thorough but concise feedback with specific suggestions and examples when helpful"
}
8 changes: 8 additions & 0 deletions fixtures/prompts/helpful-assistant.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
You are a helpful and knowledgeable assistant. Your goal is to provide accurate, concise, and useful responses to user queries.

Guidelines:
- Be clear and direct in your responses
- Provide examples when appropriate
- If you're unsure about something, say so
- Be respectful and professional at all times
- Break down complex topics into understandable explanations
17 changes: 11 additions & 6 deletions src/agent/src/InputProcessor/SystemPromptInputProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\AI\Agent\Input;
use Symfony\AI\Agent\InputProcessorInterface;
use Symfony\AI\Agent\Toolbox\ToolboxInterface;
use Symfony\AI\Platform\Message\Content\File;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Tool\Tool;
use Symfony\Contracts\Translation\TranslatableInterface;
Expand All @@ -28,11 +29,11 @@
final readonly class SystemPromptInputProcessor implements InputProcessorInterface
{
/**
* @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
* @param \Stringable|TranslatableInterface|string|File $systemPrompt the system prompt to prepend to the input messages, or a File object to read from
* @param ToolboxInterface|null $toolbox the tool box to be used to append the tool definitions to the system prompt
*/
public function __construct(
private \Stringable|TranslatableInterface|string $systemPrompt,
private \Stringable|TranslatableInterface|string|File $systemPrompt,
private ?ToolboxInterface $toolbox = null,
private ?TranslatorInterface $translator = null,
private LoggerInterface $logger = new NullLogger(),
Expand All @@ -52,9 +53,13 @@ public function processInput(Input $input): void
return;
}

$message = $this->systemPrompt instanceof TranslatableInterface
? $this->systemPrompt->trans($this->translator)
: (string) $this->systemPrompt;
if ($this->systemPrompt instanceof File) {
$message = $this->systemPrompt->asBinary();
} elseif ($this->systemPrompt instanceof TranslatableInterface) {
$message = $this->systemPrompt->trans($this->translator);
} else {
$message = (string) $this->systemPrompt;
}

if ($this->toolbox instanceof ToolboxInterface
&& [] !== $this->toolbox->getTools()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\AI\Fixtures\Tool\ToolNoParams;
use Symfony\AI\Fixtures\Tool\ToolRequiredParams;
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
use Symfony\AI\Platform\Message\Content\File;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\SystemMessage;
Expand Down Expand Up @@ -221,6 +222,49 @@ public function testWithMissingTranslator()
);
}

public function testProcessInputWithFile()
{
$tempFile = tempnam(sys_get_temp_dir(), 'prompt_');
file_put_contents($tempFile, 'This is a system prompt from a file');

try {
$file = File::fromFile($tempFile);
$processor = new SystemPromptInputProcessor($file);

$input = new Input(new Gpt('gpt-4o'), 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 system prompt from a file', $messages[0]->content);
} finally {
unlink($tempFile);
}
}

public function testProcessInputWithMultilineFile()
{
$tempFile = tempnam(sys_get_temp_dir(), 'prompt_');
file_put_contents($tempFile, "Line 1\nLine 2\nLine 3");

try {
$file = File::fromFile($tempFile);
$processor = new SystemPromptInputProcessor($file);

$input = new Input(new Gpt('gpt-4o'), 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->assertSame("Line 1\nLine 2\nLine 3", $messages[0]->content);
} finally {
unlink($tempFile);
}
}

private function getTranslator(): TranslatorInterface
{
return new class implements TranslatorInterface {
Expand Down
30 changes: 23 additions & 7 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,22 +306,35 @@
return ['text' => $v];
})
->end()
->beforeNormalization()
->ifArray()
->then(function (array $v) {
if (!isset($v['text']) && !isset($v['include_tools'])) {
throw new \InvalidArgumentException('Either "text" or "include_tools" must be configured for prompt.');
->validate()
->ifTrue(function ($v) {
if (!\is_array($v)) {
return false;
}
$hasTextOrFile = isset($v['text']) || isset($v['file']);

return $v;
return !$hasTextOrFile;
})
->thenInvalid('Either "text" or "file" must be configured for prompt.')
->end()
->validate()
->ifTrue(function ($v) {
return \is_array($v) && isset($v['text']) && isset($v['file']);
})
->thenInvalid('Cannot use both "text" and "file" for prompt. Choose one.')
->end()
->validate()
->ifTrue(function ($v) {
return \is_array($v) && '' === trim($v['text'] ?? '');
return \is_array($v) && isset($v['text']) && '' === trim($v['text']);
})
->thenInvalid('The "text" cannot be empty.')
->end()
->validate()
->ifTrue(function ($v) {
return \is_array($v) && isset($v['file']) && '' === trim($v['file']);
})
->thenInvalid('The "file" cannot be empty.')
->end()
->validate()
->ifTrue(function ($v) {
return \is_array($v) && ($v['enabled'] ?? false) && !interface_exists(TranslatorInterface::class);
Expand All @@ -332,6 +345,9 @@
->stringNode('text')
->info('The system prompt text')
->end()
->stringNode('file')
->info('Path to file containing the system prompt')
->end()
->booleanNode('include_tools')
->info('Include tool definitions at the end of the system prompt')
->defaultFalse()
Expand Down
53 changes: 52 additions & 1 deletion src/ai-bundle/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,62 @@ For more control, such as including tool definitions in the system prompt, use t

The array format supports these options:

* ``text`` (string, required): The system prompt text that will be sent to the AI model
* ``text`` (string): The system prompt text that will be sent to the AI model (either ``text`` or ``file`` is required)
* ``file`` (string): Path to a file containing the system prompt (either ``text`` or ``file`` is required)
* ``include_tools`` (boolean, optional): When set to ``true``, tool definitions will be appended to the system prompt
* ``enable_translation`` (boolean, optional): When set to ``true``, enables translation for the system prompt text (requires symfony/translation)
* ``translation_domain`` (string, optional): The translation domain to use for the system prompt translation

.. note::

You cannot use both ``text`` and ``file`` simultaneously. Choose one option based on your needs.

**File-Based Prompts**

For better organization and reusability, you can store system prompts in external files. This is particularly useful for:

* Long, complex prompts with multiple sections
* Prompts shared across multiple agents or projects
* Version-controlled prompt templates
* JSON-structured prompts with specific formatting

Configure the prompt with a file path:

.. code-block:: yaml

ai:
agent:
my_agent:
model: 'gpt-4o-mini'
prompt:
file: '%kernel.project_dir%/prompts/assistant.txt'

The file can be in any text format (.txt, .json, .md, etc.). The entire content of the file will be used as the system prompt text.

**Example Text File** (``prompts/assistant.txt``):

.. code-block:: text

You are a helpful and knowledgeable assistant.

Guidelines:
- Be clear and direct in your responses
- Provide examples when appropriate
- Be respectful and professional at all times

**Example JSON File** (``prompts/code-reviewer.json``):

.. code-block:: json

{
"role": "You are an expert code reviewer",
"responsibilities": [
"Review code for bugs and potential issues",
"Suggest improvements for code quality"
],
"tone": "constructive and educational"
}

**Translation Support**

To use translated system prompts, you need to have the Symfony Translation component installed:
Expand Down
27 changes: 21 additions & 6 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use Symfony\AI\Platform\Bridge\VertexAi\PlatformFactory as VertexAiPlatformFactory;
use Symfony\AI\Platform\Bridge\Voyage\PlatformFactory as VoyagePlatformFactory;
use Symfony\AI\Platform\Exception\RuntimeException;
use Symfony\AI\Platform\Message\Content\File;
use Symfony\AI\Platform\ModelClientInterface;
use Symfony\AI\Platform\Platform;
use Symfony\AI\Platform\PlatformInterface;
Expand Down Expand Up @@ -652,14 +653,28 @@ 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".');
}
// Create prompt from file if configured, otherwise use text
if (isset($config['prompt']['file'])) {
$filePath = $config['prompt']['file'];
// File::fromFile() handles validation, so no need to check here
// Use Definition with factory method because File objects cannot be serialized during container compilation
$prompt = (new Definition(File::class))
->setFactory([File::class, 'fromFile'])
->setArguments([$filePath]);
} elseif (isset($config['prompt']['text'])) {
$promptText = $config['prompt']['text'];

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']);
$prompt = new TranslatableMessage($promptText, domain: $config['prompt']['translation_domain']);
} else {
$prompt = $promptText;
}
} else {
$prompt = $config['prompt']['text'];
$prompt = '';
}

$systemPromptInputProcessorDefinition = (new Definition(SystemPromptInputProcessor::class))
Expand Down
22 changes: 20 additions & 2 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -835,11 +835,29 @@ public function testEmptyTextInArrayThrowsException()
]);
}

#[TestDox('System prompt array without text key throws configuration exception')]
#[TestDox('System prompt array without text or file throws configuration exception')]
public function testSystemPromptArrayWithoutTextKeyThrowsException()
{
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('The "text" cannot be empty.');
$this->expectExceptionMessage('Either "text" or "file" must be configured for prompt.');

$this->buildContainer([
'ai' => [
'agent' => [
'test_agent' => [
'model' => 'gpt-4',
'prompt' => [],
],
],
],
]);
}

#[TestDox('System prompt with only include_tools throws configuration exception')]
public function testSystemPromptWithOnlyIncludeToolsThrowsException()
{
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('Either "text" or "file" must be configured for prompt.');

$this->buildContainer([
'ai' => [
Expand Down