Skip to content

Commit f67c952

Browse files
[Platform] Integrate template rendering into Message API
Merge PromptTemplate component into Platform by integrating template rendering directly into the Message API. This provides a more cohesive developer experience where templates are rendered automatically through event listeners rather than requiring a separate component. Key changes: - Add Template class for message content with variable placeholders - Implement extensible TemplateRenderer system (string and expression) - Add TemplateRendererListener for automatic rendering via events - Register renderers and listener in AI Bundle service configuration - Remove standalone PromptTemplate component - Add comprehensive test coverage for all new functionality - Add example demonstrating template usage with different renderers The template rendering system supports both simple string replacement and complex expression evaluation, with an extensible architecture allowing custom renderers to be registered.
1 parent 00b4af3 commit f67c952

40 files changed

+1212
-1285
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
13+
use Symfony\AI\Platform\EventListener\TemplateRendererListener;
14+
use Symfony\AI\Platform\Message\Message;
15+
use Symfony\AI\Platform\Message\MessageBag;
16+
use Symfony\AI\Platform\Message\Template;
17+
use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer;
18+
use Symfony\AI\Platform\Message\TemplateRenderer\TemplateRendererRegistry;
19+
use Symfony\Component\EventDispatcher\EventDispatcher;
20+
21+
require_once dirname(__DIR__).'/bootstrap.php';
22+
23+
// Setup event dispatcher with template renderer
24+
$eventDispatcher = new EventDispatcher();
25+
$rendererRegistry = new TemplateRendererRegistry([
26+
new StringTemplateRenderer(),
27+
]);
28+
$templateListener = new TemplateRendererListener($rendererRegistry);
29+
$eventDispatcher->addSubscriber($templateListener);
30+
31+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client(), eventDispatcher: $eventDispatcher);
32+
33+
echo "Example 1: SystemMessage with template\n";
34+
echo "=======================================\n\n";
35+
36+
$template = Template::string('You are a {role} assistant.');
37+
$messages = new MessageBag(
38+
Message::forSystem($template),
39+
Message::ofUser('What is PHP?')
40+
);
41+
42+
$result = $platform->invoke('gpt-4o-mini', $messages, [
43+
'template_vars' => ['role' => 'helpful'],
44+
]);
45+
46+
echo "SystemMessage template: You are a {role} assistant.\n";
47+
echo "Variables: ['role' => 'helpful']\n";
48+
echo 'Response: '.$result->asText()."\n\n";
49+
50+
echo "Example 2: UserMessage with template\n";
51+
echo "=====================================\n\n";
52+
53+
$messages = new MessageBag(
54+
Message::forSystem('You are a helpful assistant.'),
55+
Message::ofUser(Template::string('Tell me about {topic}'))
56+
);
57+
58+
$result = $platform->invoke('gpt-4o-mini', $messages, [
59+
'template_vars' => ['topic' => 'PHP'],
60+
]);
61+
62+
echo "UserMessage template: Tell me about {topic}\n";
63+
echo "Variables: ['topic' => 'PHP']\n";
64+
echo 'Response: '.$result->asText()."\n\n";
65+
66+
echo "Example 3: AssistantMessage with template\n";
67+
echo "==========================================\n\n";
68+
69+
$assistantTemplate = Template::string('The answer is {result}');
70+
$message = Message::ofAssistant($assistantTemplate);
71+
72+
echo "AssistantMessage template: The answer is {result}\n";
73+
echo "Note: AssistantMessage templates are useful for creating conversation history\n\n";
74+
75+
echo "Example 4: Multiple messages with templates\n";
76+
echo "============================================\n\n";
77+
78+
$systemTemplate = Template::string('You are a {role} assistant.');
79+
$userTemplate = Template::string('Calculate {operation}');
80+
81+
$messages = new MessageBag(
82+
Message::forSystem($systemTemplate),
83+
Message::ofUser($userTemplate)
84+
);
85+
86+
$result = $platform->invoke('gpt-4o-mini', $messages, [
87+
'template_vars' => [
88+
'role' => 'helpful',
89+
'operation' => '2 + 2',
90+
],
91+
]);
92+
93+
echo "System template: You are a {role} assistant.\n";
94+
echo "User template: Calculate {operation}\n";
95+
echo "Variables: ['role' => 'helpful', 'operation' => '2 + 2']\n";
96+
echo 'Response: '.$result->asText()."\n\n";
97+
98+
echo "Example 5: UserMessage with mixed content\n";
99+
echo "==========================================\n\n";
100+
101+
$messages = new MessageBag(
102+
Message::forSystem('You are a helpful assistant.'),
103+
Message::ofUser('I need help with', Template::string(' {task}'))
104+
);
105+
106+
$result = $platform->invoke('gpt-4o-mini', $messages, [
107+
'template_vars' => ['task' => 'debugging'],
108+
]);
109+
110+
echo "UserMessage: 'Plain text' + Template('{task}')\n";
111+
echo "Variables: ['task' => 'debugging']\n";
112+
echo 'Response: '.$result->asText()."\n";

src/ai-bundle/config/services.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
use Symfony\AI\Platform\Contract;
6363
use Symfony\AI\Platform\Contract\JsonSchema\DescriptionParser;
6464
use Symfony\AI\Platform\Contract\JsonSchema\Factory as SchemaFactory;
65+
use Symfony\AI\Platform\EventListener\TemplateRendererListener;
66+
use Symfony\AI\Platform\Message\TemplateRenderer\ExpressionTemplateRenderer;
67+
use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer;
68+
use Symfony\AI\Platform\Message\TemplateRenderer\TemplateRendererRegistry;
6569
use Symfony\AI\Platform\Serializer\StructuredOutputSerializer;
6670
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
6771
use Symfony\AI\Platform\StructuredOutput\ResponseFormatFactory;
@@ -111,6 +115,24 @@
111115
->set('ai.platform.model_catalog.vertexai.gemini', VertexAiModelCatalog::class)
112116
->set('ai.platform.model_catalog.voyage', VoyageModelCatalog::class)
113117

118+
// message templates
119+
->set('ai.platform.template_renderer.string', StringTemplateRenderer::class)
120+
->tag('ai.platform.template_renderer')
121+
->set('ai.platform.template_renderer.expression', ExpressionTemplateRenderer::class)
122+
->args([
123+
service('expression_language')->nullOnInvalid(),
124+
])
125+
->tag('ai.platform.template_renderer')
126+
->set('ai.platform.template_renderer_registry', TemplateRendererRegistry::class)
127+
->args([
128+
tagged_iterator('ai.platform.template_renderer'),
129+
])
130+
->set('ai.platform.template_renderer_listener', TemplateRendererListener::class)
131+
->args([
132+
service('ai.platform.template_renderer_registry'),
133+
])
134+
->tag('kernel.event_subscriber')
135+
114136
// structured output
115137
->set('ai.agent.response_format_factory', ResponseFormatFactory::class)
116138
->args([

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4410,6 +4410,42 @@ public function testModelConfigurationIsIgnoredForUnknownPlatform()
44104410
$this->assertSame([], $definition->getArguments());
44114411
}
44124412

4413+
public function testTemplateRendererServicesAreRegistered()
4414+
{
4415+
$container = $this->buildContainer([
4416+
'ai' => [
4417+
'platform' => [
4418+
'anthropic' => [
4419+
'api_key' => 'test_key',
4420+
],
4421+
],
4422+
],
4423+
]);
4424+
4425+
// Verify string template renderer is registered
4426+
$this->assertTrue($container->hasDefinition('ai.platform.template_renderer.string'));
4427+
$stringRendererDefinition = $container->getDefinition('ai.platform.template_renderer.string');
4428+
$this->assertSame(\Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer::class, $stringRendererDefinition->getClass());
4429+
$this->assertTrue($stringRendererDefinition->hasTag('ai.platform.template_renderer'));
4430+
4431+
// Verify expression template renderer is registered
4432+
$this->assertTrue($container->hasDefinition('ai.platform.template_renderer.expression'));
4433+
$expressionRendererDefinition = $container->getDefinition('ai.platform.template_renderer.expression');
4434+
$this->assertSame(\Symfony\AI\Platform\Message\TemplateRenderer\ExpressionTemplateRenderer::class, $expressionRendererDefinition->getClass());
4435+
$this->assertTrue($expressionRendererDefinition->hasTag('ai.platform.template_renderer'));
4436+
4437+
// Verify template renderer registry is registered
4438+
$this->assertTrue($container->hasDefinition('ai.platform.template_renderer_registry'));
4439+
$registryDefinition = $container->getDefinition('ai.platform.template_renderer_registry');
4440+
$this->assertSame(\Symfony\AI\Platform\Message\TemplateRenderer\TemplateRendererRegistry::class, $registryDefinition->getClass());
4441+
4442+
// Verify template renderer listener is registered as event subscriber
4443+
$this->assertTrue($container->hasDefinition('ai.platform.template_renderer_listener'));
4444+
$listenerDefinition = $container->getDefinition('ai.platform.template_renderer_listener');
4445+
$this->assertSame(\Symfony\AI\Platform\EventListener\TemplateRendererListener::class, $listenerDefinition->getClass());
4446+
$this->assertTrue($listenerDefinition->hasTag('kernel.event_subscriber'));
4447+
}
4448+
44134449
private function buildContainer(array $configuration): ContainerBuilder
44144450
{
44154451
$container = new ContainerBuilder();

src/platform/AGENTS.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ Unified abstraction for AI platforms (OpenAI, Anthropic, Azure, Gemini, VertexAI
1313
- **Model**: AI models with provider-specific configurations
1414
- **Contract**: Abstract contracts for AI capabilities (chat, embedding, speech)
1515
- **Message**: Message system for AI interactions
16+
- **Template**: Message templating with pluggable rendering strategies
1617
- **Tool**: Function calling capabilities
1718
- **Bridge**: Provider-specific implementations
1819

1920
### Key Directories
2021
- `src/Bridge/`: Provider implementations
2122
- `src/Contract/`: Abstract contracts and interfaces
22-
- `src/Message/`: Message handling system
23+
- `src/Message/`: Message handling system with Template support
24+
- `src/Message/TemplateRenderer/`: Template rendering strategies
2325
- `src/Tool/`: Function calling and tool definitions
2426
- `src/Result/`: Result types and converters
2527
- `src/Exception/`: Platform-specific exceptions
@@ -54,11 +56,53 @@ composer install
5456
composer update
5557
```
5658

59+
## Usage Patterns
60+
61+
### Message Templates
62+
63+
Templates support variable substitution with type-based rendering. All message types (SystemMessage, UserMessage, AssistantMessage) support templates.
64+
65+
```php
66+
use Symfony\AI\Platform\Message\Message;
67+
use Symfony\AI\Platform\Message\MessageBag;
68+
use Symfony\AI\Platform\Message\Template;
69+
70+
// SystemMessage with template
71+
$template = Template::string('You are a {role} assistant.');
72+
$message = Message::forSystem($template);
73+
74+
// UserMessage with template
75+
$message = Message::ofUser(Template::string('Calculate {operation}'));
76+
77+
// AssistantMessage with template
78+
$message = Message::ofAssistant(Template::string('The answer is {result}'));
79+
80+
// Multiple messages with templates
81+
$messages = new MessageBag(
82+
Message::forSystem(Template::string('You are a {role} assistant.')),
83+
Message::ofUser(Template::string('Calculate {operation}'))
84+
);
85+
86+
$result = $platform->invoke('gpt-4o-mini', $messages, [
87+
'template_vars' => [
88+
'role' => 'helpful',
89+
'operation' => '2 + 2',
90+
],
91+
]);
92+
93+
// Expression template (requires symfony/expression-language)
94+
$template = Template::expression('price * quantity');
95+
```
96+
97+
Rendering happens externally during `Platform.invoke()` when `template_vars` option is provided.
98+
5799
## Development Notes
58100

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

src/platform/CLAUDE.md

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,19 @@ composer update
4444
- **Model**: Represents AI models with provider-specific configurations
4545
- **Contract**: Abstract contracts for different AI capabilities (chat, embedding, speech, etc.)
4646
- **Message**: Message system for AI interactions
47+
- **Template**: Message templating with type-based rendering strategies
4748
- **Tool**: Function calling capabilities
4849
- **Bridge**: Provider-specific implementations (OpenAI, Anthropic, etc.)
4950

5051
### Key Directories
5152
- `src/Bridge/`: Provider-specific implementations
52-
- `src/Contract/`: Abstract contracts and interfaces
53-
- `src/Message/`: Message handling system
53+
- `src/Contract/`: Abstract contracts and interfaces
54+
- `src/Message/`: Message handling system with Template support
55+
- `src/Message/TemplateRenderer/`: Template rendering strategies
5456
- `src/Tool/`: Function calling and tool definitions
5557
- `src/Result/`: Result types and converters
5658
- `src/Exception/`: Platform-specific exceptions
59+
- `src/EventListener/`: Event listeners (including TemplateRendererListener)
5760

5861
### Provider Support
5962
The component supports multiple AI providers through Bridge implementations:
@@ -66,9 +69,56 @@ The component supports multiple AI providers through Bridge implementations:
6669
- Ollama
6770
- And many others (see composer.json keywords)
6871

72+
## Usage Examples
73+
74+
### Message Templates
75+
76+
Templates support variable substitution with type-based rendering. All message types (SystemMessage, UserMessage, AssistantMessage) support templates:
77+
78+
```php
79+
use Symfony\AI\Platform\Message\Message;
80+
use Symfony\AI\Platform\Message\MessageBag;
81+
use Symfony\AI\Platform\Message\Template;
82+
83+
// SystemMessage with template
84+
$template = Template::string('You are a {role} assistant.');
85+
$message = Message::forSystem($template);
86+
87+
// UserMessage with template
88+
$message = Message::ofUser(Template::string('Calculate {operation}'));
89+
90+
// AssistantMessage with template
91+
$message = Message::ofAssistant(Template::string('The answer is {result}'));
92+
93+
// UserMessage with mixed content (text and template)
94+
$message = Message::ofUser(
95+
'Plain text',
96+
Template::string('and {dynamic} content')
97+
);
98+
99+
// Multiple messages
100+
$messages = new MessageBag(
101+
Message::forSystem(Template::string('You are a {role} assistant.')),
102+
Message::ofUser(Template::string('Calculate {operation}'))
103+
);
104+
105+
$result = $platform->invoke('gpt-4o-mini', $messages, [
106+
'template_vars' => [
107+
'role' => 'helpful',
108+
'operation' => '2 + 2',
109+
],
110+
]);
111+
112+
// Expression template (requires symfony/expression-language)
113+
$template = Template::expression('price * quantity');
114+
```
115+
116+
Templates are rendered during `Platform.invoke()` when `template_vars` option is provided.
117+
69118
## Testing Architecture
70119

71120
- Uses PHPUnit 11+ with strict configuration
72121
- Test fixtures located in `../../fixtures` for multi-modal content
73122
- Mock HTTP client pattern preferred over response mocking
74-
- Component follows Symfony coding standards
123+
- Component follows Symfony coding standards
124+
- Template tests cover all renderer types and integration scenarios

0 commit comments

Comments
 (0)