Skip to content

Conversation

@wachterjohannes
Copy link

@wachterjohannes wachterjohannes commented Nov 28, 2025

Q A
Bug fix? no
New feature? yes
Docs? yes
Issues Fix #258
License MIT

Summary

Adds a new prompt-template component to the Symfony AI monorepo, filling a gap we identified in our (Christopher Hertel and me) recent discussion about missing core functionality for prompt management.

Background

This implementation merges and adapts code from both sulu.ai and modelflow-ai projects, bringing battle-tested prompt templating into the Symfony AI ecosystem with a more extensible architecture.

Architecture

The component uses a strategy pattern for rendering, allowing different template processors to be plugged in:

  • StringRenderer (default): Simple {variable} replacement with zero dependencies
  • ExpressionRenderer: Advanced expression evaluation using Symfony Expression Language (optional)
  • Extensible: Ready for additional renderers like Twig, Mustache, or custom implementations

Key Features

  • Zero core dependencies (only PHP 8.2+)
  • Factory pattern for exceptions with typed error methods
  • Immutable readonly classes throughout
  • Interface-first design for maximum flexibility
  • Comprehensive test coverage (42 tests, 52 assertions)
  • PHPStan level 6 compliant

Usage Examples

Simple variable replacement:

$template = PromptTemplate::fromString('Hello {name}!');
echo $template->format(['name' => 'World']); // "Hello World!"

Expression-based rendering:
$renderer = new ExpressionRenderer();
$template = new PromptTemplate('Total: {price * quantity}', $renderer);
echo $template->format(['price' => 10, 'quantity' => 5]); // "Total: 50"

Custom renderer:
class TwigRenderer implements RendererInterface {
    public function render(string $template, array $values): string {
        // Twig implementation
    }
}

This provides a solid foundation for prompt template management across the Symfony AI ecosystem while maintaining the flexibility to adapt to different rendering needs.

Introduces a new prompt-template component providing simple yet extensible
prompt templating with pluggable rendering strategies.

Features:
- Zero core dependencies (only PHP 8.2+)
- StringRenderer for simple {variable} replacement (default)
- ExpressionRenderer for advanced expressions (optional, requires symfony/expression-language)
- Factory pattern for exceptions with typed error methods
- Comprehensive test coverage (42 tests, 52 assertions)
- PHPStan level 6 compliant
- Follows Symfony coding standards

Architecture:
- Strategy pattern for extensible rendering
- Immutable readonly classes throughout
- Interface-first design (PromptTemplateInterface, RendererInterface)
- Component-specific exception hierarchy

The component allows users to create prompt templates with variable substitution
using either simple string replacement or advanced expression evaluation, with
the ability to implement custom renderers for specific use cases.
@chr-hertel
Copy link
Member

chr-hertel commented Nov 29, 2025

Thanks for this @wachterjohannes - would really love to get this in 🙏

two things that we need to sort here:

  1. is it a standalone component? for that we need to sort how we would use and render the template with the Message & Platform
  2. terminology: we currently don't use the terminology "prompt", but "message" - i'd prefer to go with that here as well like MessageTemplate

we could have something like:

$template = new Symfony\AI\Platform\Message\Template::expression('Total: {price * quantity}');
$message = Message::forSystem($template);
$messages = new MessageBag($message);

$result = $platform->invoke('gpt-4o-mini', $messages, [
    'template_vars' => ['price' => 10, 'quantity' => 5],
]);

and later - while serializing - take care of the rendering - potentially throw an error on unsupported types.

with type being only a string instead of a renderer instance + named construct for promoted use:

class Template
{
    public function __construct(
        private string $template,
        private string $type,
    ) { }

    public static function string(string $template): self;
    public static function expression(string $template): self;
    public static function twig(string $template): self
    {
        return new self($template, 'twig');
    }
}

WDYT?

@wachterjohannes
Copy link
Author

@chr-hertel in my opinion an own component makes sense as it is totally independent to the other component and as you mentioned we could integrate it into the agent and platform. @OskarStark what do you think.

Regarding the renaming - makes totally sense and i will do that when the decission over the component is final :)

@chr-hertel
Copy link
Member

chr-hertel commented Nov 29, 2025

My thinking always is: API first, then implementation, then slicing. That's why i would delay that component discussion.

Templates alone are not valuable, they need an integration into the Message I'd say

@wachterjohannes
Copy link
Author

@chr-hertel both are absolutly thru statements :) so let us wait for feedback from @OskarStark

@wachterjohannes wachterjohannes changed the title [PromptTemplate] Add new component with extensible renderer architecture [Platform] Integrate template rendering into Message API Dec 7, 2025
@OskarStark
Copy link
Contributor

Lets go with the platform component first 👍

@wachterjohannes wachterjohannes force-pushed the feature/prompt-template branch from 8216936 to d7911d1 Compare December 7, 2025 21:26
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.
@wachterjohannes wachterjohannes force-pushed the feature/prompt-template branch from d7911d1 to f67c952 Compare December 7, 2025 21:27
@wachterjohannes
Copy link
Author

@OskarStark that was luck 😂 pushed a few minutes ago!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it needed for an assistant message, which comes from the LLM? 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are totally right :) will remove that from the AssistantMessage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still in

@carsonbot carsonbot changed the title [Platform] Integrate template rendering into Message API Integrate template rendering into Message API Dec 7, 2025
@wachterjohannes wachterjohannes force-pushed the feature/prompt-template branch from 6200479 to 3d1112e Compare December 7, 2025 22:00
@OskarStark OskarStark added Platform Issues & PRs about the AI Platform component AI Bundle Issues & PRs about the AI integration bundle labels Dec 7, 2025
@carsonbot carsonbot changed the title Integrate template rendering into Message API [AI Bundle][Platform] Integrate template rendering into Message API Dec 7, 2025
Copy link
Member

@chr-hertel chr-hertel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome - you pretty much nailed it - only small stuff & fabbot - thanks already!

@wachterjohannes
Copy link
Author

@chr-hertel fixed your comments

Comment on lines +4437 to +4441
if (class_exists(ExpressionLanguage::class)) {
$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'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would we ever end up here - it's not installed in the bundle, right? should we do it with req --dev?

* @param ?ToolCall[] $toolCalls
*/
public static function ofAssistant(?string $content = null, ?array $toolCalls = null): AssistantMessage
public static function ofAssistant(string|Template|null $content = null, ?array $toolCalls = null): AssistantMessage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also not needed?

Suggested change
public static function ofAssistant(string|Template|null $content = null, ?array $toolCalls = null): AssistantMessage
public static function ofAssistant(?string $content = null, ?array $toolCalls = null): AssistantMessage

/**
* @author Johannes Wachter <johannes@sulu.io>
*/
final readonly class ExpressionLanguageTemplateRenderer implements TemplateRendererInterface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missed that before

Suggested change
final readonly class ExpressionLanguageTemplateRenderer implements TemplateRendererInterface
final class ExpressionLanguageTemplateRenderer implements TemplateRendererInterface

*
* @author Johannes Wachter <johannes@sulu.io>
*/
final readonly class StringTemplateRenderer implements TemplateRendererInterface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final readonly class StringTemplateRenderer implements TemplateRendererInterface
final class StringTemplateRenderer implements TemplateRendererInterface

$this->model = new Model('gpt-4o');
}

public function testRendersTemplateWhenTemplateVarsProvided(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all those : void return type declarations on test* methods have to go, according to Symfony code style

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rename the folder to template and split into 4 different files - locally I'm executing all examples from time to time in parallel - and it will go faster like that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Bundle Issues & PRs about the AI integration bundle Feature New feature Platform Issues & PRs about the AI Platform component Status: Needs Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Agent][Platform] Introduce Prompt Templates

4 participants