Skip to content

Commit d2caecc

Browse files
committed
feature #802 [Agent][Platform] Shift structured output from Agent to Platform component (chr-hertel)
This PR was merged into the main branch. Discussion ---------- [Agent][Platform] Shift structured output from Agent to Platform component | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | no | Issues | | License | MIT The main differentiator between the Agent and the Platform component is that the Agent is designed for multi-step model interaction and this is not the case for structured output. In the beginning the feature was also a bit more heavy than the light platform, since it was using the Serializer, and the Platform not having extension points for it. Nowadays the Platform uses the Serializer anyways for the contract handling, and relies on a optional EventDispatcher, that now also can provide the needed extension points. Secondary goals where to introduce a `ResultEvent` and bring back user-land validation for structured output capability of model. Fixes #791, replaces #794 <img width="1765" height="359" alt="image" src="https://github.com/user-attachments/assets/df9fc29f-fc90-4094-8c2f-10f01535dbfa" /> Commits ------- 8110d13 Shift structured output from Agent to Platform component
2 parents ff0bc3e + 8110d13 commit d2caecc

File tree

56 files changed

+625
-409
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+625
-409
lines changed

docs/bundles/ai-bundle.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ Advanced Example with Multiple Agents
6767
agent:
6868
rag:
6969
platform: 'ai.platform.azure.gpt_deployment'
70-
structured_output: false # Disables support for "output_structure" option, default is true
7170
track_token_usage: true # Enable tracking of token usage for the agent, default is true
7271
model: 'gpt-4o-mini'
7372
memory: 'You have access to conversation history and user preferences' # Optional: static memory content
@@ -513,11 +512,6 @@ Configuration
513512
# Fallback agent for unmatched requests (required)
514513
fallback: 'general'
515514
516-
.. important::
517-
518-
The orchestrator agent MUST have ``structured_output: true`` (the default) to work correctly.
519-
The multi-agent system uses structured output to reliably parse agent selection decisions.
520-
521515
Each multi-agent configuration automatically registers a service with the ID pattern ``ai.multi_agent.{name}``.
522516

523517
For the example above, the service ``ai.multi_agent.support`` is registered and can be injected::

docs/components/agent.rst

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -483,80 +483,6 @@ Code Examples
483483
* `RAG with MongoDB`_
484484
* `RAG with Pinecone`_
485485

486-
Structured Output
487-
-----------------
488-
489-
A typical use-case of LLMs is to classify and extract data from unstructured sources, which is supported by some models
490-
by features like Structured Output or providing a Response Format.
491-
492-
PHP Classes as Output
493-
~~~~~~~~~~~~~~~~~~~~~
494-
495-
Symfony AI supports that use-case by abstracting the hustle of defining and providing schemas to the LLM and converting
496-
the result back to PHP objects.
497-
498-
To achieve this, a specific agent processor needs to be registered::
499-
500-
use Symfony\AI\Agent\Agent;
501-
use Symfony\AI\Agent\StructuredOutput\AgentProcessor;
502-
use Symfony\AI\Agent\StructuredOutput\ResponseFormatFactory;
503-
use Symfony\AI\Fixtures\StructuredOutput\MathReasoning;
504-
use Symfony\AI\Platform\Message\Message;
505-
use Symfony\AI\Platform\Message\MessageBag;
506-
use Symfony\Component\Serializer\Encoder\JsonEncoder;
507-
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
508-
use Symfony\Component\Serializer\Serializer;
509-
510-
// Initialize Platform and LLM
511-
512-
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
513-
$processor = new AgentProcessor(new ResponseFormatFactory(), $serializer);
514-
$agent = new Agent($platform, $model, [$processor], [$processor]);
515-
516-
$messages = new MessageBag(
517-
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
518-
Message::ofUser('how can I solve 8x + 7 = -23'),
519-
);
520-
$result = $agent->call($messages, ['output_structure' => MathReasoning::class]);
521-
522-
dump($result->getContent()); // returns an instance of `MathReasoning` class
523-
524-
Array Structures as Output
525-
~~~~~~~~~~~~~~~~~~~~~~~~~~
526-
527-
Also PHP array structures as response_format are supported, which also requires the agent processor mentioned above::
528-
529-
use Symfony\AI\Platform\Message\Message;
530-
use Symfony\AI\Platform\Message\MessageBag;
531-
532-
// Initialize Platform, LLM and agent with processors and Clock tool
533-
534-
$messages = new MessageBag(Message::ofUser('What date and time is it?'));
535-
$result = $agent->call($messages, ['response_format' => [
536-
'type' => 'json_schema',
537-
'json_schema' => [
538-
'name' => 'clock',
539-
'strict' => true,
540-
'schema' => [
541-
'type' => 'object',
542-
'properties' => [
543-
'date' => ['type' => 'string', 'description' => 'The current date in the format YYYY-MM-DD.'],
544-
'time' => ['type' => 'string', 'description' => 'The current time in the format HH:MM:SS.'],
545-
],
546-
'required' => ['date', 'time'],
547-
'additionalProperties' => false,
548-
],
549-
],
550-
]]);
551-
552-
dump($result->getContent()); // returns an array
553-
554-
Code Examples
555-
~~~~~~~~~~~~~
556-
557-
* `Structured Output with PHP class`_
558-
* `Structured Output with array`_
559-
560486
Input & Output Processing
561487
-------------------------
562488

@@ -825,7 +751,5 @@ Code Examples
825751
.. _`Store Component`: https://github.com/symfony/ai-store
826752
.. _`RAG with MongoDB`: https://github.com/symfony/ai/blob/main/examples/rag/mongodb.php
827753
.. _`RAG with Pinecone`: https://github.com/symfony/ai/blob/main/examples/rag/pinecone.php
828-
.. _`Structured Output with PHP class`: https://github.com/symfony/ai/blob/main/examples/openai/structured-output-math.php
829-
.. _`Structured Output with array`: https://github.com/symfony/ai/blob/main/examples/openai/structured-output-clock.php
830754
.. _`Chat with static memory`: https://github.com/symfony/ai/blob/main/examples/memory/static.php
831755
.. _`Chat with embedding search memory`: https://github.com/symfony/ai/blob/main/examples/memory/mariadb.php

docs/components/platform.rst

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,76 @@ Code Examples
278278
* `Embeddings with Voyage`_
279279
* `Embeddings with Mistral`_
280280

281+
Structured Output
282+
-----------------
283+
284+
A typical use-case of LLMs is to classify and extract data from unstructured sources, which is supported by some models
285+
by features like Structured Output or providing a Response Format.
286+
287+
PHP Classes as Output
288+
~~~~~~~~~~~~~~~~~~~~~
289+
290+
Symfony AI supports that use-case by abstracting the hustle of defining and providing schemas to the LLM and converting
291+
the result back to PHP objects.
292+
293+
To achieve this, the ``Symfony\AI\Platform\StructuredOutput\PlatformSubscriber`` needs to be registered with the platform::
294+
295+
use Symfony\AI\Fixtures\StructuredOutput\MathReasoning;
296+
use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory;
297+
use Symfony\AI\Platform\Message\Message;
298+
use Symfony\AI\Platform\Message\MessageBag;
299+
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
300+
use Symfony\Component\EventDispatcher\EventDispatcher;
301+
302+
$dispatcher = new EventDispatcher();
303+
$dispatcher->addSubscriber(new PlatformSubscriber());
304+
305+
$platform = PlatformFactory::create($apiKey, eventDispatcher: $dispatcher);
306+
$messages = new MessageBag(
307+
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
308+
Message::ofUser('how can I solve 8x + 7 = -23'),
309+
);
310+
$result = $platform->invoke('mistral-small-latest', $messages, ['output_structure' => MathReasoning::class]);
311+
312+
dump($result->asObject()); // returns an instance of `MathReasoning` class
313+
314+
Array Structures as Output
315+
~~~~~~~~~~~~~~~~~~~~~~~~~~
316+
317+
Also PHP array structures as response_format are supported, which also requires the event subscriber mentioned above. On
318+
top this example uses the feature through the agent to leverage tool calling::
319+
320+
use Symfony\AI\Platform\Message\Message;
321+
use Symfony\AI\Platform\Message\MessageBag;
322+
323+
// Initialize Platform, LLM and agent with processors and Clock tool
324+
325+
$messages = new MessageBag(Message::ofUser('What date and time is it?'));
326+
$result = $agent->call($messages, ['response_format' => [
327+
'type' => 'json_schema',
328+
'json_schema' => [
329+
'name' => 'clock',
330+
'strict' => true,
331+
'schema' => [
332+
'type' => 'object',
333+
'properties' => [
334+
'date' => ['type' => 'string', 'description' => 'The current date in the format YYYY-MM-DD.'],
335+
'time' => ['type' => 'string', 'description' => 'The current time in the format HH:MM:SS.'],
336+
],
337+
'required' => ['date', 'time'],
338+
'additionalProperties' => false,
339+
],
340+
],
341+
]]);
342+
343+
dump($result->getContent()); // returns an array
344+
345+
Code Examples
346+
~~~~~~~~~~~~~
347+
348+
* `Structured Output with PHP class`_
349+
* `Structured Output with array`_
350+
281351
Server Tools
282352
------------
283353

@@ -426,6 +496,8 @@ Code Examples
426496
.. _`Embeddings with OpenAI`: https://github.com/symfony/ai/blob/main/examples/openai/embeddings.php
427497
.. _`Embeddings with Voyage`: https://github.com/symfony/ai/blob/main/examples/voyage/embeddings.php
428498
.. _`Embeddings with Mistral`: https://github.com/symfony/ai/blob/main/examples/mistral/embeddings.php
499+
.. _`Structured Output with PHP class`: https://github.com/symfony/ai/blob/main/examples/openai/structured-output-math.php
500+
.. _`Structured Output with array`: https://github.com/symfony/ai/blob/main/examples/openai/structured-output-clock.php
429501
.. _`Parallel GPT Calls`: https://github.com/symfony/ai/blob/main/examples/misc/parallel-chat-gpt.php
430502
.. _`Parallel Embeddings Calls`: https://github.com/symfony/ai/blob/main/examples/misc/parallel-embeddings.php
431503
.. _`LM Studio`: https://lmstudio.ai/

examples/deepseek/structured-output-clock.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,27 @@
1010
*/
1111

1212
use Symfony\AI\Agent\Agent;
13-
use Symfony\AI\Agent\StructuredOutput\AgentProcessor as StructuredOutputProcessor;
14-
use Symfony\AI\Agent\Toolbox\AgentProcessor as ToolProcessor;
13+
use Symfony\AI\Agent\Toolbox\AgentProcessor;
1514
use Symfony\AI\Agent\Toolbox\Tool\Clock;
1615
use Symfony\AI\Agent\Toolbox\Toolbox;
1716
use Symfony\AI\Platform\Bridge\DeepSeek\PlatformFactory;
1817
use Symfony\AI\Platform\Message\Message;
1918
use Symfony\AI\Platform\Message\MessageBag;
19+
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
2020
use Symfony\Component\Clock\Clock as SymfonyClock;
21+
use Symfony\Component\EventDispatcher\EventDispatcher;
2122

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

24-
$platform = PlatformFactory::create(env('DEEPSEEK_API_KEY'), http_client());
25+
$dispatcher = new EventDispatcher();
26+
$dispatcher->addSubscriber(new PlatformSubscriber());
27+
28+
$platform = PlatformFactory::create(env('DEEPSEEK_API_KEY'), http_client(), eventDispatcher: $dispatcher);
2529

2630
$clock = new Clock(new SymfonyClock());
27-
$toolbox = new Toolbox([$clock]);
28-
$toolProcessor = new ToolProcessor($toolbox);
29-
$structuredOutputProcessor = new StructuredOutputProcessor();
30-
$agent = new Agent($platform, 'deepseek-chat', [$toolProcessor, $structuredOutputProcessor], [$toolProcessor, $structuredOutputProcessor]);
31+
$toolbox = new Toolbox([$clock], logger: logger());
32+
$toolProcessor = new AgentProcessor($toolbox);
33+
$agent = new Agent($platform, 'deepseek-chat', [$toolProcessor], [$toolProcessor]);
3134

3235
$messages = new MessageBag(
3336
// for DeepSeek it is *mandatory* to mention JSON anywhere in the prompt when using structured output

examples/gemini/structured-output-clock.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,27 @@
1010
*/
1111

1212
use Symfony\AI\Agent\Agent;
13-
use Symfony\AI\Agent\StructuredOutput\AgentProcessor as StructuredOutputProcessor;
14-
use Symfony\AI\Agent\Toolbox\AgentProcessor as ToolProcessor;
13+
use Symfony\AI\Agent\Toolbox\AgentProcessor;
1514
use Symfony\AI\Agent\Toolbox\Tool\Clock;
1615
use Symfony\AI\Agent\Toolbox\Toolbox;
1716
use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory;
1817
use Symfony\AI\Platform\Message\Message;
1918
use Symfony\AI\Platform\Message\MessageBag;
19+
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
2020
use Symfony\Component\Clock\Clock as SymfonyClock;
21+
use Symfony\Component\EventDispatcher\EventDispatcher;
2122

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

24-
$platform = PlatformFactory::create(env('GEMINI_API_KEY'), http_client());
25+
$dispatcher = new EventDispatcher();
26+
$dispatcher->addSubscriber(new PlatformSubscriber());
27+
28+
$platform = PlatformFactory::create(env('GEMINI_API_KEY'), http_client(), eventDispatcher: $dispatcher);
2529

2630
$clock = new Clock(new SymfonyClock());
2731
$toolbox = new Toolbox([$clock], logger: logger());
28-
$toolProcessor = new ToolProcessor($toolbox);
29-
$structuredOutputProcessor = new StructuredOutputProcessor();
30-
$agent = new Agent($platform, 'gemini-2.5-flash', [$toolProcessor, $structuredOutputProcessor], [$toolProcessor, $structuredOutputProcessor]);
32+
$toolProcessor = new AgentProcessor($toolbox);
33+
$agent = new Agent($platform, 'gemini-2.5-flash', [$toolProcessor], [$toolProcessor]);
3134

3235
$messages = new MessageBag(Message::ofUser('What date and time is it?'));
3336
$result = $agent->call($messages, ['response_format' => [

examples/gemini/structured-output-math.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,24 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
use Symfony\AI\Agent\Agent;
13-
use Symfony\AI\Agent\StructuredOutput\AgentProcessor;
1412
use Symfony\AI\Fixtures\StructuredOutput\MathReasoning;
1513
use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory;
1614
use Symfony\AI\Platform\Message\Message;
1715
use Symfony\AI\Platform\Message\MessageBag;
16+
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
17+
use Symfony\Component\EventDispatcher\EventDispatcher;
1818

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

21-
$platform = PlatformFactory::create(env('GEMINI_API_KEY'), http_client());
21+
$dispatcher = new EventDispatcher();
22+
$dispatcher->addSubscriber(new PlatformSubscriber());
2223

23-
$processor = new AgentProcessor();
24-
$agent = new Agent($platform, 'gemini-2.5-flash', [$processor], [$processor]);
24+
$platform = PlatformFactory::create(env('GEMINI_API_KEY'), http_client(), eventDispatcher: $dispatcher);
2525
$messages = new MessageBag(
2626
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
2727
Message::ofUser('how can I solve 8x + 7 = -23'),
2828
);
29-
$result = $agent->call($messages, ['output_structure' => MathReasoning::class]);
3029

31-
dump($result->getContent());
30+
$result = $platform->invoke('gemini-2.5-flash', $messages, ['output_structure' => MathReasoning::class]);
31+
32+
dump($result->asObject());

examples/mistral/structured-output-math.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,23 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
use Symfony\AI\Agent\Agent;
13-
use Symfony\AI\Agent\StructuredOutput\AgentProcessor;
14-
use Symfony\AI\Agent\StructuredOutput\ResponseFormatFactory;
1512
use Symfony\AI\Fixtures\StructuredOutput\MathReasoning;
1613
use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory;
1714
use Symfony\AI\Platform\Message\Message;
1815
use Symfony\AI\Platform\Message\MessageBag;
19-
use Symfony\Component\Serializer\Encoder\JsonEncoder;
20-
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
21-
use Symfony\Component\Serializer\Serializer;
16+
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
17+
use Symfony\Component\EventDispatcher\EventDispatcher;
2218

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

25-
$platform = PlatformFactory::create(env('MISTRAL_API_KEY'), http_client());
21+
$dispatcher = new EventDispatcher();
22+
$dispatcher->addSubscriber(new PlatformSubscriber());
2623

27-
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
28-
29-
$processor = new AgentProcessor(new ResponseFormatFactory(), $serializer);
30-
$agent = new Agent($platform, 'mistral-small-latest', [$processor], [$processor]);
24+
$platform = PlatformFactory::create(env('MISTRAL_API_KEY'), http_client(), eventDispatcher: $dispatcher);
3125
$messages = new MessageBag(
3226
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
3327
Message::ofUser('how can I solve 8x + 7 = -23'),
3428
);
35-
$result = $agent->call($messages, ['output_structure' => MathReasoning::class]);
29+
$result = $platform->invoke('mistral-small-latest', $messages, ['output_structure' => MathReasoning::class]);
3630

37-
dump($result->getContent());
31+
dump($result->asObject());

examples/multi-agent/orchestrator.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,23 @@
1313
use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor;
1414
use Symfony\AI\Agent\MultiAgent\Handoff;
1515
use Symfony\AI\Agent\MultiAgent\MultiAgent;
16-
use Symfony\AI\Agent\StructuredOutput\AgentProcessor;
1716
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
1817
use Symfony\AI\Platform\Message\Message;
1918
use Symfony\AI\Platform\Message\MessageBag;
19+
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
20+
use Symfony\Component\EventDispatcher\EventDispatcher;
2021

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

23-
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
24-
25-
// Create structured output processor for the orchestrator
26-
$structuredOutputProcessor = new AgentProcessor();
24+
$dispatcher = new EventDispatcher();
25+
$dispatcher->addSubscriber(new PlatformSubscriber());
26+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client(), eventDispatcher: $dispatcher);
2727

2828
// Create orchestrator agent for routing decisions
2929
$orchestrator = new Agent(
3030
$platform,
3131
'gpt-4o-mini',
32-
[new SystemPromptInputProcessor('You are an intelligent agent orchestrator that routes user questions to specialized agents.'), $structuredOutputProcessor],
33-
[$structuredOutputProcessor],
32+
[new SystemPromptInputProcessor('You are an intelligent agent orchestrator that routes user questions to specialized agents.')],
3433
);
3534

3635
// Create technical agent for handling technical issues

examples/ollama/structured-output-math.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
use Symfony\AI\Agent\Agent;
13-
use Symfony\AI\Agent\StructuredOutput\AgentProcessor;
1412
use Symfony\AI\Fixtures\StructuredOutput\MathReasoning;
1513
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory;
1614
use Symfony\AI\Platform\Message\Message;
1715
use Symfony\AI\Platform\Message\MessageBag;
16+
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
17+
use Symfony\Component\EventDispatcher\EventDispatcher;
1818

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

21-
$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client());
21+
$dispatcher = new EventDispatcher();
22+
$dispatcher->addSubscriber(new PlatformSubscriber());
2223

23-
$processor = new AgentProcessor();
24-
$agent = new Agent($platform, env('OLLAMA_LLM'), [$processor], [$processor]);
24+
$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client(), eventDispatcher: $dispatcher);
2525
$messages = new MessageBag(
2626
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
2727
Message::ofUser('how can I solve 8x + 7 = -23'),
2828
);
29-
$result = $agent->call($messages, ['output_structure' => MathReasoning::class]);
29+
$result = $platform->invoke(env('OLLAMA_LLM'), $messages, ['output_structure' => MathReasoning::class]);
3030

31-
dump($result->getContent());
31+
dump($result->asObject());

0 commit comments

Comments
 (0)