Skip to content

Commit 1d77c97

Browse files
VincentLangletchr-hertel
authored andcommitted
[AI Bundle][Agent] Add #[AsInputProcessor] and #[AsOutputProcessor]
1 parent a071848 commit 1d77c97

File tree

7 files changed

+336
-25
lines changed

7 files changed

+336
-25
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
namespace Symfony\AI\Agent\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
15+
final readonly class AsInputProcessor
16+
{
17+
/**
18+
* @param string|null $agent the service id of the agent which will use this processor,
19+
* null to register this processor for all existing agents
20+
*/
21+
public function __construct(
22+
public ?string $agent = null,
23+
public int $priority = 0,
24+
) {
25+
}
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
namespace Symfony\AI\Agent\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
15+
final readonly class AsOutputProcessor
16+
{
17+
/**
18+
* @param string|null $agent the service id of the agent which will use this processor,
19+
* null to register this processor for all existing agents
20+
*/
21+
public function __construct(
22+
public ?string $agent = null,
23+
public int $priority = 0,
24+
) {
25+
}
26+
}

src/ai-bundle/config/services.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@
6868
service('ai.agent.response_format_factory'),
6969
service('serializer'),
7070
])
71-
->tag('ai.agent.input_processor')
72-
->tag('ai.agent.output_processor')
7371

7472
// tools
7573
->set('ai.toolbox.abstract', Toolbox::class)
@@ -111,8 +109,6 @@
111109
])
112110
->set('ai.tool.agent_processor', ToolProcessor::class)
113111
->parent('ai.tool.agent_processor.abstract')
114-
->tag('ai.agent.input_processor')
115-
->tag('ai.agent.output_processor')
116112
->arg('index_0', service('ai.toolbox'))
117113
->set('ai.security.is_granted_attribute_listener', IsGrantedToolAttributeListener::class)
118114
->args([

src/ai-bundle/doc/index.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,34 @@ Use the `Agent` service to leverage models and tools::
150150
}
151151
}
152152

153+
**Register Processors**
154+
155+
By default, all services implementing the ``InputProcessorInterface`` or the
156+
``OutputProcessorInterface`` interfaces are automatically applied to every ``Agent``.
157+
158+
This behavior can be overridden/configured with the ``#[AsInputProcessor]`` and
159+
the ``#[AsOutputProcessor]`` attributes::
160+
161+
use Symfony\AI\Agent\Input;
162+
use Symfony\AI\Agent\InputProcessorInterface;
163+
use Symfony\AI\Agent\Output;
164+
use Symfony\AI\Agent\OutputProcessorInterface;
165+
166+
#[AsInputProcessor(priority: 99)] // This applies to every agent
167+
#[AsOutputProcessor(agent: 'my_agent_id')] // The output processor will only be registered for 'my_agent_id'
168+
final readonly class MyService implements InputProcessorInterface, OutputProcessorInterface
169+
{
170+
public function processInput(Input $input): void
171+
{
172+
// ...
173+
}
174+
175+
public function processOutput(Output $output): void
176+
{
177+
// ...
178+
}
179+
}
180+
153181
**Register Tools**
154182

155183
To use existing tools, you can register them as a service:

src/ai-bundle/src/AiBundle.php

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Google\Auth\ApplicationDefaultCredentials;
1515
use Symfony\AI\Agent\Agent;
1616
use Symfony\AI\Agent\AgentInterface;
17+
use Symfony\AI\Agent\Attribute\AsInputProcessor;
18+
use Symfony\AI\Agent\Attribute\AsOutputProcessor;
1719
use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor;
1820
use Symfony\AI\Agent\InputProcessorInterface;
1921
use Symfony\AI\Agent\OutputProcessorInterface;
@@ -22,6 +24,7 @@
2224
use Symfony\AI\Agent\Toolbox\Tool\Agent as AgentTool;
2325
use Symfony\AI\Agent\Toolbox\ToolFactory\ChainFactory;
2426
use Symfony\AI\Agent\Toolbox\ToolFactory\MemoryToolFactory;
27+
use Symfony\AI\AiBundle\DependencyInjection\ProcessorCompilerPass;
2528
use Symfony\AI\AiBundle\Exception\InvalidArgumentException;
2629
use Symfony\AI\AiBundle\Profiler\TraceablePlatform;
2730
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
@@ -83,6 +86,13 @@
8386
*/
8487
final class AiBundle extends AbstractBundle
8588
{
89+
public function build(ContainerBuilder $container)
90+
{
91+
parent::build($container);
92+
93+
$container->addCompilerPass(new ProcessorCompilerPass());
94+
}
95+
8696
public function configure(DefinitionConfigurator $definition): void
8797
{
8898
$definition->import('../config/options.php');
@@ -143,10 +153,25 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
143153
]);
144154
});
145155

156+
$builder->registerAttributeForAutoconfiguration(AsInputProcessor::class, static function (ChildDefinition $definition, AsInputProcessor $attribute): void {
157+
$definition->addTag('ai.agent.input_processor', [
158+
'agent' => $attribute->agent,
159+
'priority' => $attribute->priority,
160+
]);
161+
});
162+
163+
$builder->registerAttributeForAutoconfiguration(AsOutputProcessor::class, static function (ChildDefinition $definition, AsOutputProcessor $attribute): void {
164+
$definition->addTag('ai.agent.output_processor', [
165+
'agent' => $attribute->agent,
166+
'priority' => $attribute->priority,
167+
]);
168+
});
169+
146170
$builder->registerForAutoconfiguration(InputProcessorInterface::class)
147-
->addTag('ai.agent.input_processor');
171+
->addTag('ai.agent.input_processor', ['tagged_by' => 'interface']);
148172
$builder->registerForAutoconfiguration(OutputProcessorInterface::class)
149-
->addTag('ai.agent.output_processor');
173+
->addTag('ai.agent.output_processor', ['tagged_by' => 'interface']);
174+
150175
$builder->registerForAutoconfiguration(ModelClientInterface::class)
151176
->addTag('ai.platform.model_client');
152177
$builder->registerForAutoconfiguration(ResultConverterInterface::class)
@@ -436,9 +461,6 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
436461
->setArgument(0, new Reference($config['platform']))
437462
->setArgument(1, new Reference('ai.agent.'.$name.'.model'));
438463

439-
$inputProcessors = [];
440-
$outputProcessors = [];
441-
442464
// TOOL & PROCESSOR
443465
if ($config['tools']['enabled']) {
444466
// Create specific toolbox and process if tools are explicitly defined
@@ -492,26 +514,28 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
492514

493515
$toolProcessorDefinition = (new ChildDefinition('ai.tool.agent_processor.abstract'))
494516
->replaceArgument(0, new Reference('ai.toolbox.'.$name));
495-
$container->setDefinition('ai.tool.agent_processor.'.$name, $toolProcessorDefinition);
496517

497-
$inputProcessors[] = new Reference('ai.tool.agent_processor.'.$name);
498-
$outputProcessors[] = new Reference('ai.tool.agent_processor.'.$name);
518+
$container->setDefinition('ai.tool.agent_processor.'.$name, $toolProcessorDefinition)
519+
->addTag('ai.agent.input_processor', ['agent' => $name, 'priority' => -10])
520+
->addTag('ai.agent.output_processor', ['agent' => $name, 'priority' => -10]);
499521
} else {
500522
if ($config['fault_tolerant_toolbox'] && !$container->hasDefinition('ai.fault_tolerant_toolbox')) {
501523
$container->setDefinition('ai.fault_tolerant_toolbox', new Definition(FaultTolerantToolbox::class))
502524
->setArguments([new Reference('.inner')])
503525
->setDecoratedService('ai.toolbox');
504526
}
505527

506-
$inputProcessors[] = new Reference('ai.tool.agent_processor');
507-
$outputProcessors[] = new Reference('ai.tool.agent_processor');
528+
$container->getDefinition('ai.tool.agent_processor')
529+
->addTag('ai.agent.input_processor', ['agent' => $name, 'priority' => -10])
530+
->addTag('ai.agent.output_processor', ['agent' => $name, 'priority' => -10]);
508531
}
509532
}
510533

511534
// STRUCTURED OUTPUT
512535
if ($config['structured_output']) {
513-
$inputProcessors[] = new Reference('ai.agent.structured_output_processor');
514-
$outputProcessors[] = new Reference('ai.agent.structured_output_processor');
536+
$container->getDefinition('ai.agent.structured_output_processor')
537+
->addTag('ai.agent.input_processor', ['agent' => $name, 'priority' => -20])
538+
->addTag('ai.agent.output_processor', ['agent' => $name, 'priority' => -20]);
515539
}
516540

517541
// TOKEN USAGE TRACKING
@@ -530,25 +554,28 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
530554
}
531555

532556
if ($container->hasDefinition('ai.platform.token_usage_processor.'.$platform)) {
533-
$outputProcessors[] = new Reference('ai.platform.token_usage_processor.'.$platform);
557+
$container->getDefinition('ai.platform.token_usage_processor.'.$platform)
558+
->addTag('ai.agent.output_processor', ['agent' => $name, 'priority' => -30]);
534559
}
535560
}
536561
}
537562

538563
// SYSTEM PROMPT
539564
if (\is_string($config['system_prompt'])) {
540-
$systemPromptInputProcessorDefinition = new Definition(SystemPromptInputProcessor::class, [
541-
$config['system_prompt'],
542-
$config['include_tools'] ? new Reference('ai.toolbox.'.$name) : null,
543-
new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
544-
]);
565+
$systemPromptInputProcessorDefinition = (new Definition(SystemPromptInputProcessor::class))
566+
->setArguments([
567+
$config['system_prompt'],
568+
$config['include_tools'] ? new Reference('ai.toolbox.'.$name) : null,
569+
new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
570+
])
571+
->addTag('ai.agent.input_processor', ['agent' => $name, 'priority' => -30]);
545572

546-
$inputProcessors[] = $systemPromptInputProcessorDefinition;
573+
$container->setDefinition('ai.agent.'.$name.'.system_prompt_processor', $systemPromptInputProcessorDefinition);
547574
}
548575

549576
$agentDefinition
550-
->setArgument(2, $inputProcessors)
551-
->setArgument(3, $outputProcessors)
577+
->setArgument(2, []) // placeholder until ProcessorCompilerPass process.
578+
->setArgument(3, []) // placeholder until ProcessorCompilerPass process.
552579
->setArgument(4, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))
553580
;
554581

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
namespace Symfony\AI\AiBundle\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
18+
class ProcessorCompilerPass implements CompilerPassInterface
19+
{
20+
public function process(ContainerBuilder $container): void
21+
{
22+
$inputProcessors = $container->findTaggedServiceIds('ai.agent.input_processor');
23+
$outputProcessors = $container->findTaggedServiceIds('ai.agent.output_processor');
24+
25+
foreach ($container->findTaggedServiceIds('ai.agent') as $serviceId => $tags) {
26+
$agentInputProcessors = [];
27+
$agentOutputProcessors = [];
28+
foreach ($inputProcessors as $processorId => $processorTags) {
29+
foreach ($processorTags as $tag) {
30+
if ('interface' === ($tag['tagged_by'] ?? null) && \count($processorTags) > 1) {
31+
continue;
32+
}
33+
34+
$agent = $tag['agent'] ?? null;
35+
if (null === $agent || $agent === $serviceId) {
36+
$priority = $tag['priority'] ?? 0;
37+
$agentInputProcessors[] = [$priority, new Reference($processorId)];
38+
}
39+
}
40+
}
41+
42+
foreach ($outputProcessors as $processorId => $processorTags) {
43+
foreach ($processorTags as $tag) {
44+
if ('interface' === ($tag['tagged_by'] ?? null) && \count($processorTags) > 1) {
45+
continue;
46+
}
47+
48+
$agent = $tag['agent'] ?? null;
49+
if (null === $agent || $agent === $serviceId) {
50+
$priority = $tag['priority'] ?? 0;
51+
$agentOutputProcessors[] = [$priority, new Reference($processorId)];
52+
}
53+
}
54+
}
55+
56+
$sortCb = static fn (array $a, array $b): int => $b[0] <=> $a[0];
57+
usort($agentInputProcessors, $sortCb);
58+
usort($agentOutputProcessors, $sortCb);
59+
60+
$agentDefinition = $container->getDefinition($serviceId);
61+
$agentDefinition
62+
->setArgument(2, array_column($agentInputProcessors, 1))
63+
->setArgument(3, array_column($agentOutputProcessors, 1));
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)