Skip to content
Open
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
47 changes: 47 additions & 0 deletions examples/platform/template/01-system-message.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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\Platform\Bridge\OpenAi\PlatformFactory;
use Symfony\AI\Platform\EventListener\TemplateRendererListener;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\Template;
use Symfony\AI\Platform\Message\TemplateRenderer\ChainTemplateRenderer;
use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer;
use Symfony\Component\EventDispatcher\EventDispatcher;

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

$eventDispatcher = new EventDispatcher();
$rendererRegistry = new ChainTemplateRenderer([
new StringTemplateRenderer(),
]);
$templateListener = new TemplateRendererListener($rendererRegistry);
$eventDispatcher->addSubscriber($templateListener);

$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client(), eventDispatcher: $eventDispatcher);

echo "SystemMessage with template\n";
echo "===========================\n\n";

$template = Template::string('You are a {domain} expert assistant.');
$messages = new MessageBag(
Message::forSystem($template),
Message::ofUser('What is PHP?')
);

$result = $platform->invoke('gpt-4o-mini', $messages, [
'template_vars' => ['domain' => 'programming'],
]);

echo "SystemMessage template: You are a {domain} expert assistant.\n";
echo "Variables: ['domain' => 'programming']\n";
echo 'Response: '.$result->asText()."\n";
46 changes: 46 additions & 0 deletions examples/platform/template/02-user-message.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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\Platform\Bridge\OpenAi\PlatformFactory;
use Symfony\AI\Platform\EventListener\TemplateRendererListener;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\Template;
use Symfony\AI\Platform\Message\TemplateRenderer\ChainTemplateRenderer;
use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer;
use Symfony\Component\EventDispatcher\EventDispatcher;

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

$eventDispatcher = new EventDispatcher();
$rendererRegistry = new ChainTemplateRenderer([
new StringTemplateRenderer(),
]);
$templateListener = new TemplateRendererListener($rendererRegistry);
$eventDispatcher->addSubscriber($templateListener);

$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client(), eventDispatcher: $eventDispatcher);

echo "UserMessage with template\n";
echo "=========================\n\n";

$messages = new MessageBag(
Message::forSystem('You are a helpful assistant.'),
Message::ofUser(Template::string('Tell me about {topic}'))
);

$result = $platform->invoke('gpt-4o-mini', $messages, [
'template_vars' => ['topic' => 'PHP'],
]);

echo "UserMessage template: Tell me about {topic}\n";
echo "Variables: ['topic' => 'PHP']\n";
echo 'Response: '.$result->asText()."\n";
53 changes: 53 additions & 0 deletions examples/platform/template/03-multiple-messages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?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\Platform\Bridge\OpenAi\PlatformFactory;
use Symfony\AI\Platform\EventListener\TemplateRendererListener;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\Template;
use Symfony\AI\Platform\Message\TemplateRenderer\ChainTemplateRenderer;
use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer;
use Symfony\Component\EventDispatcher\EventDispatcher;

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

$eventDispatcher = new EventDispatcher();
$rendererRegistry = new ChainTemplateRenderer([
new StringTemplateRenderer(),
]);
$templateListener = new TemplateRendererListener($rendererRegistry);
$eventDispatcher->addSubscriber($templateListener);

$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client(), eventDispatcher: $eventDispatcher);

echo "Multiple messages with templates\n";
echo "=================================\n\n";

$systemTemplate = Template::string('You are a {domain} assistant.');
$userTemplate = Template::string('Calculate {operation}');

$messages = new MessageBag(
Message::forSystem($systemTemplate),
Message::ofUser($userTemplate)
);

$result = $platform->invoke('gpt-4o-mini', $messages, [
'template_vars' => [
'domain' => 'math',
'operation' => '2 + 2',
],
]);

echo "System template: You are a {domain} assistant.\n";
echo "User template: Calculate {operation}\n";
echo "Variables: ['domain' => 'math', 'operation' => '2 + 2']\n";
echo 'Response: '.$result->asText()."\n";
46 changes: 46 additions & 0 deletions examples/platform/template/04-mixed-content.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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\Platform\Bridge\OpenAi\PlatformFactory;
use Symfony\AI\Platform\EventListener\TemplateRendererListener;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\Template;
use Symfony\AI\Platform\Message\TemplateRenderer\ChainTemplateRenderer;
use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer;
use Symfony\Component\EventDispatcher\EventDispatcher;

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

$eventDispatcher = new EventDispatcher();
$rendererRegistry = new ChainTemplateRenderer([
new StringTemplateRenderer(),
]);
$templateListener = new TemplateRendererListener($rendererRegistry);
$eventDispatcher->addSubscriber($templateListener);

$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client(), eventDispatcher: $eventDispatcher);

echo "UserMessage with mixed content\n";
echo "==============================\n\n";

$messages = new MessageBag(
Message::forSystem('You are a helpful assistant.'),
Message::ofUser('I need help with', Template::string(' {task}'))
);

$result = $platform->invoke('gpt-4o-mini', $messages, [
'template_vars' => ['task' => 'debugging'],
]);

echo "UserMessage: 'Plain text' + Template('{task}')\n";
echo "Variables: ['task' => 'debugging']\n";
echo 'Response: '.$result->asText()."\n";
29 changes: 29 additions & 0 deletions src/ai-bundle/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
use Symfony\AI\Platform\Contract;
use Symfony\AI\Platform\Contract\JsonSchema\DescriptionParser;
use Symfony\AI\Platform\Contract\JsonSchema\Factory as SchemaFactory;
use Symfony\AI\Platform\EventListener\TemplateRendererListener;
use Symfony\AI\Platform\Message\TemplateRenderer\ChainTemplateRenderer;
use Symfony\AI\Platform\Message\TemplateRenderer\ExpressionLanguageTemplateRenderer;
use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer;
use Symfony\AI\Platform\Serializer\StructuredOutputSerializer;
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
use Symfony\AI\Platform\StructuredOutput\ResponseFormatFactory;
Expand All @@ -70,6 +74,7 @@
use Symfony\AI\Store\Command\IndexCommand;
use Symfony\AI\Store\Command\RetrieveCommand;
use Symfony\AI\Store\Command\SetupStoreCommand;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

return static function (ContainerConfigurator $container): void {
$container->services()
Expand Down Expand Up @@ -112,6 +117,30 @@
->set('ai.platform.model_catalog.vertexai.gemini', VertexAiModelCatalog::class)
->set('ai.platform.model_catalog.voyage', VoyageModelCatalog::class)

// message templates
->set('ai.platform.template_renderer.string', StringTemplateRenderer::class)
->tag('ai.platform.template_renderer');

if (class_exists(ExpressionLanguage::class)) {
$container->services()
->set('ai.platform.template_renderer.expression', ExpressionLanguageTemplateRenderer::class)
->args([
service('expression_language'),
])
->tag('ai.platform.template_renderer');
}

$container->services()
->set('ai.platform.template_renderer_registry', ChainTemplateRenderer::class)
->args([
tagged_iterator('ai.platform.template_renderer'),
])
->set('ai.platform.template_renderer_listener', TemplateRendererListener::class)
->args([
service('ai.platform.template_renderer_registry'),
])
->tag('kernel.event_subscriber')

// structured output
->set('ai.agent.response_format_factory', ResponseFormatFactory::class)
->args([
Expand Down
41 changes: 41 additions & 0 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory;
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
use Symfony\AI\Platform\Capability;
use Symfony\AI\Platform\EventListener\TemplateRendererListener;
use Symfony\AI\Platform\Message\TemplateRenderer\ChainTemplateRenderer;
use Symfony\AI\Platform\Message\TemplateRenderer\ExpressionLanguageTemplateRenderer;
use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\PlatformInterface;
use Symfony\AI\Store\Bridge\AzureSearch\SearchStore as AzureStore;
Expand Down Expand Up @@ -72,6 +76,7 @@
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;

Expand Down Expand Up @@ -6853,6 +6858,42 @@ public function testModelConfigurationIsIgnoredForUnknownPlatform()
$this->assertSame([], $definition->getArguments());
}

public function testTemplateRendererServicesAreRegistered()
{
$container = $this->buildContainer([
'ai' => [
'platform' => [
'anthropic' => [
'api_key' => 'test_key',
],
],
],
]);

// Verify string template renderer is registered
$this->assertTrue($container->hasDefinition('ai.platform.template_renderer.string'));
$stringRendererDefinition = $container->getDefinition('ai.platform.template_renderer.string');
$this->assertSame(StringTemplateRenderer::class, $stringRendererDefinition->getClass());
$this->assertTrue($stringRendererDefinition->hasTag('ai.platform.template_renderer'));

// Verify expression template renderer is registered
$this->assertTrue($container->hasDefinition('ai.platform.template_renderer.expression'));
$expressionRendererDefinition = $container->getDefinition('ai.platform.template_renderer.expression');
$this->assertSame(ExpressionLanguageTemplateRenderer::class, $expressionRendererDefinition->getClass());
$this->assertTrue($expressionRendererDefinition->hasTag('ai.platform.template_renderer'));

// Verify template renderer registry is registered
$this->assertTrue($container->hasDefinition('ai.platform.template_renderer_registry'));
$registryDefinition = $container->getDefinition('ai.platform.template_renderer_registry');
$this->assertSame(ChainTemplateRenderer::class, $registryDefinition->getClass());

// Verify template renderer listener is registered as event subscriber
$this->assertTrue($container->hasDefinition('ai.platform.template_renderer_listener'));
$listenerDefinition = $container->getDefinition('ai.platform.template_renderer_listener');
$this->assertSame(TemplateRendererListener::class, $listenerDefinition->getClass());
$this->assertTrue($listenerDefinition->hasTag('kernel.event_subscriber'));
}

private function buildContainer(array $configuration): ContainerBuilder
{
$container = new ContainerBuilder();
Expand Down
45 changes: 43 additions & 2 deletions src/platform/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ Unified abstraction for AI platforms (OpenAI, Anthropic, Azure, Gemini, VertexAI
- **Model**: AI models with provider-specific configurations
- **Contract**: Abstract contracts for AI capabilities (chat, embedding, speech)
- **Message**: Message system for AI interactions
- **Template**: Message templating with pluggable rendering strategies
- **Tool**: Function calling capabilities
- **Bridge**: Provider-specific implementations

### Key Directories
- `src/Bridge/`: Provider implementations
- `src/Contract/`: Abstract contracts and interfaces
- `src/Message/`: Message handling system
- `src/Message/`: Message handling system with Template support
- `src/Message/TemplateRenderer/`: Template rendering strategies
- `src/Tool/`: Function calling and tool definitions
- `src/Result/`: Result types and converters
- `src/Exception/`: Platform-specific exceptions
Expand Down Expand Up @@ -54,11 +56,50 @@ composer install
composer update
```

## Usage Patterns

### Message Templates

Templates support variable substitution with type-based rendering. SystemMessage and UserMessage support templates.

```php
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Message\Template;

// SystemMessage with template
$template = Template::string('You are a {role} assistant.');
$message = Message::forSystem($template);

// UserMessage with template
$message = Message::ofUser(Template::string('Calculate {operation}'));

// Multiple messages with templates
$messages = new MessageBag(
Message::forSystem(Template::string('You are a {role} assistant.')),
Message::ofUser(Template::string('Calculate {operation}'))
);

$result = $platform->invoke('gpt-4o-mini', $messages, [
'template_vars' => [
'role' => 'helpful',
'operation' => '2 + 2',
],
]);

// Expression template (requires symfony/expression-language)
$template = Template::expression('price * quantity');
```

Rendering happens externally during `Platform.invoke()` when `template_vars` option is provided.

## Development Notes

- PHPUnit 11+ with strict configuration
- Test fixtures in `../../fixtures` for multimodal content
- MockHttpClient pattern preferred
- Follows Symfony coding standards
- Bridge pattern for provider implementations
- Consistent contract interfaces across providers
- Consistent contract interfaces across providers
- Template system uses type-based rendering (not renderer injection)
- Template rendering via TemplateRendererListener during invocation
Loading
Loading