Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Symfony AI in Action - SymfonyLive Berlin 2026

Symfony AI in Action - SymfonyLive Berlin 2026

At first AI is a ton of buzzwords and hype, but how do we integrate AI in our existing products / software projects in a meaningful and realistic manner?
Let's have a look how Symfony AI enables us to bring AI-based or even agentic features, not only chatbots, into our PHP applications or scaled architecture.
If you are looking for input on how to approach AI, need ideas where to get started or find out what's realistic at first - let this talk be your inspiration!

This talk was given at SymfonyLive Berlin 2026 - https://live.symfony.com/2026-berlin/

Avatar for Christopher Hertel

Christopher Hertel

April 24, 2026

More Decks by Christopher Hertel

Other Decks in Programming

Transcript

  1. 1

  2. Christopher Hertel Software Engineer in Berlin Focus on PHP, Symfony

    and GenAI Working on Tech Program of Active on Symfony AI & MCP PHP SDK 3
  3. Components Foundation Components Platform Agent Store Tools AI Bundle Framework

    Integration Third Party more than 60 Integrations already 60 MCP Bundle Framework Integration MCP SDK Server - Client 8
  4. Platform Component Abstraction Layer for Model Platforms Central Interface for

    Inference Model Platform Bridges Multi-Modal & Streaming Support Structured Output Integrated Event System 11
  5. Platform Component Contract Invokation DTOs Platform Provider Provider ... Model

    Inference Model Inference Contract ... Result DTOs Input - Text - Messages - Image - Audio Result - Text - Binary - Stream - Object Provider - 1:n Relation - Model Catalog - Capabilities - Models.dev Model Inference - Mostly HTTP - Remote & local - Coding Agents - "Native" PHP 12
  6. Model Inference use Symfony\AI\Platform\Bridge\OpenAi\Factory; use Symfony\AI\Platform\Message\{Message, MessageBag}; $platform = Factory::createPlatform($apiKey);

    $input = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), ); $result = $platform->invoke('gpt-5-mini', $input); echo $result->asText(); 5 6 7 8 9 10 11 12 13 14 15 16 17 13
  7. OpenAI GPT use Symfony\AI\Platform\Bridge\OpenAi\Factory; $result = $platform->invoke('gpt-5-mini', $input); 5 use

    Symfony\AI\Platform\Message\{Message, MessageBag}; 6 7 $platform = Factory::createPlatform($apiKey); 8 9 $input = new MessageBag( 10 Message::forSystem('You are a pirate and you write funny.'), 11 Message::ofUser('What is the Symfony framework?'), 12 ); 13 14 15 16 echo $result->asText(); 17 14
  8. Anthropic Claude use Symfony\AI\Platform\Bridge\Anthropic\Factory; $result = $platform->invoke('claude-sonnet-4-5-20250929', $input); 5 use

    Symfony\AI\Platform\Message\{Message, MessageBag}; 6 7 $platform = Factory::createPlatform($apiKey); 8 9 $input = new MessageBag( 10 Message::forSystem('You are a pirate and you write funny.'), 11 Message::ofUser('What is the Symfony framework?'), 12 ); 13 14 15 16 echo $result->asText(); 17 15
  9. Google Gemini use Symfony\AI\Platform\Bridge\Gemini\Factory; $result = $platform->invoke('gemini-3-pro-preview', $input); 5 use

    Symfony\AI\Platform\Message\{Message, MessageBag}; 6 7 $platform = Factory::createPlatform($apiKey); 8 9 $input = new MessageBag( 10 Message::forSystem('You are a pirate and you write funny.'), 11 Message::ofUser('What is the Symfony framework?'), 12 ); 13 14 15 16 echo $result->asText(); 17 16
  10. Mistral use Symfony\AI\Platform\Bridge\Mistral\Factory; $result = $platform->invoke('mistral-large-latest', $input); 5 use Symfony\AI\Platform\Message\{Message,

    MessageBag}; 6 7 $platform = Factory::createPlatform($apiKey); 8 9 $input = new MessageBag( 10 Message::forSystem('You are a pirate and you write funny.'), 11 Message::ofUser('What is the Symfony framework?'), 12 ); 13 14 15 16 echo $result->asText(); 17 17
  11. Streaming $result = $platform->invoke('mistral-large-latest', $input, [ 'stream' => true, ]);

    foreach ($result->asTextStream() as $delta) { echo $delta; } $input = new MessageBag( 10 Message::forSystem('You are a pirate and you write funny.'), 11 Message::ofUser('What is the Symfony framework?'), 12 ); 13 14 15 16 17 18 19 20 21 18
  12. Typed Streaming Deltas foreach ($result->asStream() as $delta) { if ($delta

    instanceof TextDelta) { echo $delta; } if ($delta instanceof ThinkingDelta) { echo "<info>" . $delta->getThinking() . "</info>"; } if ($delta instanceof ToolCallComplete) { $logger->info('tool invoked', ['name' => $delta->getName()]); } if ($delta instanceof TokenUsage) { $metrics->record($delta->getTotalTokens()); } } 19
  13. Prompt Templates $input = new MessageBag( Message::forSystem('You are a helpful

    assistant.'), Message::ofUser(Template::string('Tell me about {topic}')), ); $result = $platform->invoke('gpt-5-mini', $input, [ 'template_vars' => ['topic' => 'Symfony AI'], ]); echo $result->asText(); 20
  14. Audio, Image & PDF Input $input = new MessageBag( Message::ofUser(

    'Describe the audio file.', Audio::fromDataUrl('data:audio/mpeg;base64,/9j/4AAQ...'), ), ); $result = $platform->invoke('gemini-2.5-flash', $input); echo $result->asText(); 10 11 12 13 14 15 16 17 18 19 Message::ofUser( 'Describe the audio file.', Audio::fromDataUrl('data:audio/mpeg;base64,/9j/4AAQ...'), ), $input = new MessageBag( 10 11 12 13 14 ); 15 16 $result = $platform->invoke('gemini-2.5-flash', $input); 17 18 echo $result->asText(); 19 21
  15. Audio, Image & PDF Input Message::ofUser( 'What is visible on

    the image?', new ImageUrl('https://example.org/elephant.jpg'), ), $input = new MessageBag( 10 11 12 13 14 ); 15 16 $result = $platform->invoke('gemini-2.5-flash', $input); 17 18 echo $result->asText(); 19 22
  16. Audio, Image & PDF Input Message::ofUser( 'Describe the content of

    the PDF file.', Document::fromFile('/path/to/document.pdf'), ), $input = new MessageBag( 10 11 12 13 14 ); 15 16 $result = $platform->invoke('gemini-2.5-flash', $input); 17 18 echo $result->asText(); 19 23
  17. Binary Output $input = new MessageBag( Message::ofUser( 'Colorize the elephant

    in red.', Image::fromFile('/path/to/elephant.jpg'), ), ); $result = $platform->invoke('gemini-2.5-flash-image', $input); $result->asFile('/tmp/colorized.png'); → 24
  18. Token Usage Also exposed as TokenUsage delta in streaming results.

    $result = $platform->invoke('gpt-5-mini', $input); $tokenUsage = $result->getMetadata()->get('token_usage'); echo $tokenUsage->getPromptTokens(); // 23 echo $tokenUsage->getCompletionTokens(); // 142 echo $tokenUsage->getTotalTokens(); // 165 25
  19. Structured Output $input = new MessageBag( Message::forSystem('Help users as math

    tutor, step by step.'), Message::ofUser('how can I solve 8x + 7 = -23'), ); $result = $platform->invoke('mistral-small-latest', $input, [ 'response_format' => MathReasoning::class, ]); dump($result->asObject()); 26
  20. Object Population $city = new City(name: 'Berlin'); $input = new

    MessageBag(Message::ofUser( Template::string('Provide missing information for {city.name}'), )); $platform->invoke('gpt-5-mini', $input, [ 'template_vars' => ['city' => $city], 'response_format' => $city, ]); echo $city->getMajor(); Kai Wegner 28
  21. Embeddings Models Semantic Vectors are LLM's little sibling $result =

    $platform->invoke('text-embedding-3-small', $text); echo $result->asVectors(); // array of floats 29
  22. Hugging Face Integration with Hugging Face's Inference Hub API use

    Symfony\AI\Platform\Bridge\HuggingFace\Task; $result = $platform->invoke('facebook/detr-resnet-50', $image, [ 'task' => Task::OBJECT_DETECTION, ]); echo $result->asObject(); // image classification result 30
  23. Platform Events ModelRoutingEvent Before model resolution — observe or override

    provider routing InvocationEvent Before the platform call — mutate messages or options ResultEvent After the deferred result is created — react to the pending response StartEvent / DeltaEvent / CompleteEvent Streaming lifecycle for listeners on asStream() 32
  24. Platform Configuration # config/packages/ai.yaml ai: platform: openai: api_key: '%env(OPENAI_API_KEY)%' anthropic:

    api_key: '%env(ANTHROPIC_API_KEY)%' # test command bin/console ai:platform:invoke openai gpt-5-mini "Hello Berlin" 35
  25. Agent Component Multi-Step Interaction with Model and App Combining Platform

    & Model Collection of Messages/Prompts as Context Memory, Tools, and more Various Architectures From static workflows to autonomous orchestration 38
  26. Agent Component Platform Model Inference Agent Tool Tool Tool Agent

    Agent defines - Instructions - Context - Tools - Model Model - Generates a response - Potentially invoking a tool Agent executes - Tool calls - Returns results to model Model - Generates a response - Ends with a stop response 39
  27. Tool Calling use Doctrine\ORM\EntityManagerInterface; use Symfony\AI\Agent\Toolbox\Attribute\AsTool; #[AsTool('cookbook_create_recipe', 'Creates a new

    recipe in the cookbook.')] final readonly class CookbookCreateRecipe { public function __construct( private EntityManagerInterface $entityManager, ) {} /** * @param Recipe $recipe The recipe to create in the cookbook. */ public function __invoke(Recipe $recipe): string { // validate & persist recipe ... return 'Recipe created successfully!'; } } 40
  28. Tool Argument Validation Requires symfony/validator to be installed. use Symfony\Component\Validator\Constraints

    as Assert; final readonly class Recipe { public function __construct( #[Assert\NotBlank] public string $name, #[Assert\GreaterThanOrEqual(1)] public int $servings, /** @var string[] */ #[Assert\All([new Assert\NotBlank()])] public array $ingredients, ) {} } 41
  29. Agent Configuration # config/packages/ai.yaml ai: agent: wikipedia: model: name: 'gpt-5-mini'

    options: temperature: 0.2 prompt: 'You are a helpful research assistant.' tools: - 'Symfony\AI\Agent\Bridge\Wikipedia\Wikipedia' # test command bin/console ai:agent:call wikipedia Prompts can be a file, translation key, or service reference. 42
  30. Agent Events ToolCallRequested Before tool runs — stoppable, listener can

    deny with a reason ToolCallArgumentsResolved Arguments deserialized — validator listener hooks in here ToolCallSucceeded / ToolCallFailed Per-tool result, with arguments, return value, or exception ToolCallsExecuted Aggregate after a batch of tool calls finishes 44
  31. Human in the Loop With ToolCallRequested tool calls are stoppable!

    use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested; $dispatcher->addListener(ToolCallRequested::class, function (ToolCall $toolCall = $event->getToolCall(); $approved = $io->confirm(sprintf( "Allow tool '%s' with args %s?", $toolCall->getName(), json_encode($toolCall->getArguments()), )); if (!$approved) { $event->deny('User denied tool execution.'); } }); 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $dispatcher->addListener(ToolCallRequested::class, function (ToolCall use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested; 5 6 7 $toolCall = $event->getToolCall(); 8 $approved = $io->confirm(sprintf( 9 "Allow tool '%s' with args %s?", 10 $toolCall->getName(), 11 json_encode($toolCall->getArguments()), 12 )); 13 14 if (!$approved) { 15 $event->deny('User denied tool execution.'); 16 } 17 }); 18 $approved = $io->confirm(sprintf( "Allow tool '%s' with args %s?", $toolCall->getName(), json_encode($toolCall->getArguments()), )); use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested; 5 6 $dispatcher->addListener(ToolCallRequested::class, function (ToolCall 7 $toolCall = $event->getToolCall(); 8 9 10 11 12 13 14 if (!$approved) { 15 $event->deny('User denied tool execution.'); 16 } 17 }); 18 if (!$approved) { $event->deny('User denied tool execution.'); } use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested; 5 6 $dispatcher->addListener(ToolCallRequested::class, function (ToolCall 7 $toolCall = $event->getToolCall(); 8 $approved = $io->confirm(sprintf( 9 "Allow tool '%s' with args %s?", 10 $toolCall->getName(), 11 json_encode($toolCall->getArguments()), 12 )); 13 14 15 16 17 }); 18 45
  32. Multi-Agent Architectures Level of Autonomy & Context Sharing Implemented Workflows

    Solved in user land Subagents as Tools Defined subtask & isolated context Orchestration with Handoff Central orchestrator agent & handoffs with context to subagents 46
  33. Subagent as Tool # config/packages/ai.yaml ai: # Platform Configuration ...

    agent: researcher: model: 'gpt-5' prompt: 'You do heavy research.' smalltalker: model: 'gpt-5-mini' tools: # Agent as tool 🤯 - agent: 'researcher' name: 'research_topic' description: 'Does research on a given topic.' 47
  34. Orchestration # config/packages/ai.yaml ai: # Platform Configuration ... agent: orchestrator:

    { # agent config ... } technical: { # agent config ... } billing: { # agent config ... } fallback: { # agent config ... } multi_agent: support: orchestrator: 'orchestrator' handoffs: technical: ['bug', 'error', 'code', 'debug'] billing: ['invoice', 'payment', 'bill', 'order'] fallback: 'fallback' 48
  35. Memory Register the provider as a service and reference it

    from ai.agent.*.memory.service. final class UserFactsMemory implements MemoryProviderInterface { public function __construct( private UserRepository $user, private Security $security, ) {} public function load(Input $input): array { $facts = $this->user->findFacts($this->security->getUser()); return $facts ? [ new Memory('## Known about the user'.\PHP_EOL.$facts) ] : []; } } 49
  36. Memory Configuration # config/packages/ai.yaml ai: agent: blog: model: 'gpt-5-mini' prompt:

    'You are a helpful booking agent.' tools: - 'App\Tool\BookingSearch' memory: service: 'App\Memory\UserFactsMemory' 50
  37. Speech Agent Configuration Currently, a SpeechAgent decorator: STT → Agent

    → TTS # config/packages/ai.yaml ai: agent: voice: model: 'gpt-5-mini' prompt: | You are a friendly voice assistant. Keep answers concise. speech: speech_to_text_platform: 'ai.platform.openai' stt_model: 'whisper-1' text_to_speech_platform: 'ai.platform.elevenlabs' tts_model: 'eleven_multilingual_v2' tts_options: voice: 'Bill' 51
  38. Store Component Vector Store Abstraction Layer Lifecycle Management Adding &

    Querying for Documents Document Indexing Pipeline 1. Loading 2. Filtering 3. Transforming 4. Vectorizing 54
  39. Store Configuration # config/packages/ai.yaml ai: store: postgres: blog: dbal_connection: 'doctrine.dbal.default_connection'

    table_name: 'blog_vectors' vectorizer: openai: platform: 'ai.platform.openai' model: 'text-embedding-3-small' retriever: blog: store: 'ai.store.postgres.blog' vectorizer: 'ai.vectorizer.openai' 55
  40. Indexing Pipeline $vectorizer = new Vectorizer($platform, 'text-embedding-3-small'); $indexer = new

    SourceIndexer( loader: new TextFileLoader(), processor: new DocumentProcessor( vectorizer: $vectorizer, store: $store, transformers: [ new TextReplaceTransformer(search: '## Plot', replace: '## Synopsis'), new TextSplitTransformer(chunkSize: 500, overlap: 100), ], ), ); $indexer->index(['/docs/gladiator.md', '/docs/inception.md']); 57
  41. Indexer Configuration # config/packages/ai.yaml ai: indexer: blog: loader: 'Symfony\AI\Store\Document\Loader\RssFeedLoader' source:

    'https://feeds.feedburner.com/symfony/blog' transformers: - 'Symfony\AI\Store\Document\Transformer\TextSplitTransformer' vectorizer: 'ai.vectorizer.openai' store: 'ai.store.postgres.blog' # indexing command bin/console ai:store:index blog 58
  42. Loaders & Transformers Loaders TextFileLoader MarkdownLoader CsvLoader JsonFileLoader RssFeedLoader RstLoader

    / RstToctreeLoader InMemoryLoader Transformers TextSplitTransformer TextReplaceTransformer TextTrimTransformer ChunkDelayTransformer SummaryGeneratorTransformer ChainTransformer 59
  43. Retriever & Queries $retriever = new Retriever($store, $vectorizer); // High-level:

    text in, documents out (wraps TextQuery) $documents = $retriever->retrieve('mafia movie'); // Low-level: pick the Query shape yourself $store->query(new TextQuery('mafia movie')); $store->query(new VectorQuery($vectorizer->vectorize('mafia movie'))); $store->query(new HybridQuery( vector: $vectorizer->vectorize('mafia movie'), text: 'mafia movie', semanticRatio: 0.7, // 70% semantic, 30% keyword )); 5 6 7 8 9 10 11 12 13 14 15 16 17 $retriever = new Retriever($store, $vectorizer); 5 6 // High-level: text in, documents out (wraps TextQuery) 7 $documents = $retriever->retrieve('mafia movie'); 8 9 // Low-level: pick the Query shape yourself 10 $store->query(new TextQuery('mafia movie')); 11 $store->query(new VectorQuery($vectorizer->vectorize('mafia movie'))); 12 $store->query(new HybridQuery( 13 vector: $vectorizer->vectorize('mafia movie'), 14 text: 'mafia movie', 15 semanticRatio: 0.7, // 70% semantic, 30% keyword 16 )); 17 $documents = $retriever->retrieve('mafia movie'); $retriever = new Retriever($store, $vectorizer); 5 6 // High-level: text in, documents out (wraps TextQuery) 7 8 9 // Low-level: pick the Query shape yourself 10 $store->query(new TextQuery('mafia movie')); 11 $store->query(new VectorQuery($vectorizer->vectorize('mafia movie'))); 12 $store->query(new HybridQuery( 13 vector: $vectorizer->vectorize('mafia movie'), 14 text: 'mafia movie', 15 semanticRatio: 0.7, // 70% semantic, 30% keyword 16 )); 17 $store->query(new TextQuery('mafia movie')); $retriever = new Retriever($store, $vectorizer); 5 6 // High-level: text in, documents out (wraps TextQuery) 7 $documents = $retriever->retrieve('mafia movie'); 8 9 // Low-level: pick the Query shape yourself 10 11 $store->query(new VectorQuery($vectorizer->vectorize('mafia movie'))); 12 $store->query(new HybridQuery( 13 vector: $vectorizer->vectorize('mafia movie'), 14 text: 'mafia movie', 15 semanticRatio: 0.7, // 70% semantic, 30% keyword 16 )); 17 $store->query(new VectorQuery($vectorizer->vectorize('mafia movie'))); $retriever = new Retriever($store, $vectorizer); 5 6 // High-level: text in, documents out (wraps TextQuery) 7 $documents = $retriever->retrieve('mafia movie'); 8 9 // Low-level: pick the Query shape yourself 10 $store->query(new TextQuery('mafia movie')); 11 12 $store->query(new HybridQuery( 13 vector: $vectorizer->vectorize('mafia movie'), 14 text: 'mafia movie', 15 semanticRatio: 0.7, // 70% semantic, 30% keyword 16 )); 17 $store->query(new HybridQuery( vector: $vectorizer->vectorize('mafia movie'), text: 'mafia movie', semanticRatio: 0.7, // 70% semantic, 30% keyword )); $retriever = new Retriever($store, $vectorizer); 5 6 // High-level: text in, documents out (wraps TextQuery) 7 $documents = $retriever->retrieve('mafia movie'); 8 9 // Low-level: pick the Query shape yourself 10 $store->query(new TextQuery('mafia movie')); 11 $store->query(new VectorQuery($vectorizer->vectorize('mafia movie'))); 12 13 14 15 16 17 60
  44. Store Events PreQueryEvent Before documents are retrieved — enhance or

    rewrite the query PostQueryEvent After documents are retrieved — filter, rerank, or enrich results 61
  45. SimilaritySearch Configuration # config/packages/ai.yaml ai: agent: blog: model: 'gpt-5-mini' prompt:

    'Answer using the similarity_search tool.' tools: - 'Symfony\AI\Agent\Bridge\SimilaritySearch\SimilaritySearch' 62
  46. Symfony AI Roadmap Multi-Agent Architectures Agent Traces Evaluations & Benchmarking

    Parallelization / Orchestration Support for Protocols like A2A & AG-UI Tool Discovery Built-in Mercure Support Better Docs, Cookbook, and Demo ... 68
  47. Get Started with Symfony AI Checkout symfony/ai repository Especially examples/

    and demo/ folders Build your first feature & give feedback See you on GitHub.com! 71