Skip to content

Commit 8f8a195

Browse files
OskarStarkclaude
andcommitted
Add multi-agent bundle configuration with validation
- Add minimum 2 handoffs validation to MultiAgent - Configure multi-agent systems via AiBundle YAML configuration - Add comprehensive documentation for multi-agent bundle config - Update orchestrator example to use AgentInterface in handoffs - Make delegation log message more concise - Remove trailing newline from composer.json 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d91c48f commit 8f8a195

File tree

7 files changed

+217
-5
lines changed

7 files changed

+217
-5
lines changed

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,3 @@
1717
"sort-packages": true
1818
}
1919
}
20-

demo/config/packages/ai.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ ai:
4949
- agent: 'blog'
5050
name: 'symfony_blog'
5151
description: 'Can answer questions based on the Symfony blog.'
52+
orchestrator:
53+
model:
54+
class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'
55+
name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_4O_MINI
56+
prompt: 'You are an intelligent agent orchestrator that routes user questions to specialized agents.'
57+
tools: false
58+
technical:
59+
model:
60+
class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'
61+
name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_4O_MINI
62+
prompt: 'You are a technical support specialist. Help users resolve bugs, problems, and technical errors.'
63+
tools: false
64+
general:
65+
model:
66+
class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'
67+
name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_4O_MINI
68+
prompt: 'You are a helpful general assistant. Assist users with any questions or tasks they may have.'
69+
tools: false
5270
store:
5371
chroma_db:
5472
symfonycon:
@@ -68,6 +86,14 @@ ai:
6886
- 'Symfony\AI\Store\Document\Transformer\TextTrimTransformer'
6987
vectorizer: 'ai.vectorizer.openai'
7088
store: 'ai.store.chroma_db.symfonycon'
89+
multi_agent:
90+
support:
91+
orchestrator: 'ai.agent.orchestrator'
92+
handoffs:
93+
- to: 'ai.agent.technical'
94+
when: ['bug', 'problem', 'technical', 'error', 'code', 'debug']
95+
- to: 'ai.agent.general'
96+
when: []
7197

7298
services:
7399
_defaults:

examples/multi-agent/orchestrator.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,9 @@
5050

5151
$multiAgent = new MultiAgent(
5252
orchestrator: $orchestrator,
53-
agents: [$technical, $general],
5453
handoffs: [
55-
new Handoff(to: 'technical', when: ['bug', 'problem', 'technical', 'error']),
56-
new Handoff(to: 'general'),
54+
new Handoff(to: $technical, when: ['bug', 'problem', 'technical', 'error']),
55+
new Handoff(to: $general),
5756
],
5857
logger: logger()
5958
);

src/agent/src/MultiAgent/MultiAgent.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public function __construct(
4545
if ([] === $handoffs) {
4646
throw new InvalidArgumentException('Handoffs array cannot be empty.');
4747
}
48+
49+
if (\count($handoffs) < 2) {
50+
throw new InvalidArgumentException('MultiAgent requires at least 2 handoffs. For a single handoff, use the agent directly.');
51+
}
4852
}
4953

5054
public function getName(): string
@@ -119,7 +123,7 @@ public function call(MessageBag $messages, array $options = []): ResultInterface
119123
return $this->orchestrator->call($messages, $options);
120124
}
121125

122-
$this->logger->debug('MultiAgent: Delegating to target agent', ['agent_name' => $agentName]);
126+
$this->logger->debug('MultiAgent: Delegating to agent', ['agent_name' => $agentName]);
123127
$originalMessages = new MessageBag(self::findUserMessage($userMessages));
124128

125129
return $targetAgent->call($originalMessages, $options);

src/ai-bundle/config/options.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,40 @@
594594
->end()
595595
->end()
596596
->end()
597+
->arrayNode('multi_agent')
598+
->info('Multi-agent orchestration configuration')
599+
->useAttributeAsKey('name')
600+
->arrayPrototype()
601+
->children()
602+
->stringNode('orchestrator')
603+
->info('Service ID of the orchestrator agent')
604+
->isRequired()
605+
->end()
606+
->arrayNode('handoffs')
607+
->info('Handoff rules for agent routing')
608+
->isRequired()
609+
->requiresAtLeastOneElement()
610+
->arrayPrototype()
611+
->children()
612+
->stringNode('to')
613+
->info('Service ID of the target agent')
614+
->isRequired()
615+
->end()
616+
->arrayNode('when')
617+
->info('Keywords or phrases that trigger this handoff')
618+
->scalarPrototype()->end()
619+
->defaultValue([])
620+
->end()
621+
->end()
622+
->end()
623+
->end()
624+
->stringNode('logger')
625+
->info('Service ID of the logger')
626+
->defaultNull()
627+
->end()
628+
->end()
629+
->end()
630+
->end()
597631
->end()
598632
;
599633
};

src/ai-bundle/docs/multi-agent.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Multi-Agent Configuration
2+
3+
The AI Bundle provides a configuration system for creating multi-agent orchestrators that route requests to specialized agents based on defined handoff rules.
4+
5+
## Configuration
6+
7+
Configure multi-agent systems in your `config/packages/ai.yaml` file:
8+
9+
```yaml
10+
ai:
11+
multi_agent:
12+
# Define named multi-agent systems
13+
support:
14+
# The main orchestrator agent that analyzes requests
15+
orchestrator: 'ai.agent.orchestrator'
16+
17+
# Handoff rules defining when to route to specific agents
18+
# Minimum 2 handoffs required (otherwise use the agent directly)
19+
handoffs:
20+
# Route to technical agent for specific keywords
21+
- to: 'ai.agent.technical'
22+
when: ['bug', 'problem', 'technical', 'error', 'code', 'debug']
23+
24+
# Fallback to general agent when no specific conditions match
25+
- to: 'ai.agent.general'
26+
when: []
27+
28+
# Optional: Custom logger service
29+
logger: 'monolog.logger.ai'
30+
```
31+
32+
## Service Registration
33+
34+
Each multi-agent configuration automatically registers a service with the ID pattern `ai.multi_agent.{name}`.
35+
36+
For the example above, the service `ai.multi_agent.support` is registered and can be injected:
37+
38+
```php
39+
use Symfony\AI\Agent\AgentInterface;
40+
use Symfony\Component\Routing\Attribute\Route;
41+
use Symfony\Contracts\Service\Attribute\Target;
42+
43+
class SupportController
44+
{
45+
public function __construct(
46+
#[Target('supportMultiAgent')]
47+
private AgentInterface $supportAgent,
48+
) {
49+
}
50+
51+
#[Route('/ask-support')]
52+
public function askSupport(string $question): Response
53+
{
54+
$messages = new MessageBag(Message::ofUser($question));
55+
$response = $this->supportAgent->call($messages);
56+
57+
return new Response($response->getContent());
58+
}
59+
}
60+
```
61+
62+
## Handoff Rules
63+
64+
Handoff rules determine when to delegate to specific agents:
65+
66+
### `to` (required)
67+
The service ID of the target agent to delegate to.
68+
69+
### `when` (optional)
70+
An array of keywords or phrases that trigger this handoff. When the orchestrator identifies these keywords in the user's request, it delegates to the specified agent.
71+
72+
If `when` is empty or not specified, the handoff acts as a fallback for requests that don't match other rules.
73+
74+
## How It Works
75+
76+
1. The orchestrator agent receives the initial request
77+
2. It analyzes the request content and matches it against handoff rules
78+
3. If keywords match a handoff's `when` conditions, the request is delegated to that agent
79+
4. If no specific conditions match, a fallback handoff (with empty `when`) is used
80+
5. The delegated agent processes the request and returns the response
81+
82+
## Requirements
83+
84+
- At least 2 handoff rules must be defined (for a single handoff, use the agent directly)
85+
- The orchestrator and all referenced agents must be registered as services
86+
- All agent services must implement `Symfony\AI\Agent\AgentInterface`
87+
88+
## Example: Customer Service Bot
89+
90+
```yaml
91+
ai:
92+
multi_agent:
93+
customer_service:
94+
orchestrator: 'ai.agent.analyzer'
95+
handoffs:
96+
# Technical support
97+
- to: 'ai.agent.tech_support'
98+
when: ['error', 'bug', 'crash', 'not working', 'broken']
99+
100+
# Billing inquiries
101+
- to: 'ai.agent.billing'
102+
when: ['payment', 'invoice', 'billing', 'subscription', 'price']
103+
104+
# Product information
105+
- to: 'ai.agent.product_info'
106+
when: ['features', 'how to', 'tutorial', 'guide', 'documentation']
107+
108+
# General inquiries (fallback)
109+
- to: 'ai.agent.general_support'
110+
when: []
111+
```

src/ai-bundle/src/AiBundle.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
use Symfony\AI\Agent\InputProcessorInterface;
2222
use Symfony\AI\Agent\Memory\MemoryInputProcessor;
2323
use Symfony\AI\Agent\Memory\StaticMemoryProvider;
24+
use Symfony\AI\Agent\MultiAgent\Handoff;
25+
use Symfony\AI\Agent\MultiAgent\MultiAgent;
2426
use Symfony\AI\Agent\OutputProcessorInterface;
2527
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
2628
use Symfony\AI\Agent\Toolbox\FaultTolerantToolbox;
@@ -164,6 +166,10 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
164166
$builder->setAlias(IndexerInterface::class, 'ai.indexer.'.$indexerName);
165167
}
166168

169+
foreach ($config['multi_agent'] ?? [] as $multiAgentName => $multiAgent) {
170+
$this->processMultiAgentConfig($multiAgentName, $multiAgent, $builder);
171+
}
172+
167173
$builder->registerAttributeForAutoconfiguration(AsTool::class, static function (ChildDefinition $definition, AsTool $attribute): void {
168174
$definition->addTag('ai.tool', [
169175
'name' => $attribute->name,
@@ -1203,4 +1209,37 @@ private function processIndexerConfig(int|string $name, array $config, Container
12031209

12041210
$container->setDefinition('ai.indexer.'.$name, $definition);
12051211
}
1212+
1213+
/**
1214+
* @param array<string, mixed> $config
1215+
*/
1216+
private function processMultiAgentConfig(string $name, array $config, ContainerBuilder $container): void
1217+
{
1218+
$handoffs = [];
1219+
1220+
foreach ($config['handoffs'] as $handoffConfig) {
1221+
$agentReference = new Reference($handoffConfig['to']);
1222+
1223+
$handoffDefinition = new Definition(Handoff::class, [
1224+
$agentReference,
1225+
$handoffConfig['when'] ?? [],
1226+
]);
1227+
1228+
$handoffs[] = $handoffDefinition;
1229+
}
1230+
1231+
$multiAgentId = 'ai.multi_agent.'.$name;
1232+
$multiAgentDefinition = new Definition(MultiAgent::class, [
1233+
new Reference($config['orchestrator']),
1234+
$handoffs,
1235+
$name,
1236+
new Reference($config['logger'] ?? 'logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
1237+
]);
1238+
1239+
$multiAgentDefinition->addTag('ai.multi_agent', ['name' => $name]);
1240+
$multiAgentDefinition->addTag('ai.agent', ['name' => $name]);
1241+
1242+
$container->setDefinition($multiAgentId, $multiAgentDefinition);
1243+
$container->registerAliasForArgument($multiAgentId, AgentInterface::class, (new Target($name.'MultiAgent'))->getParsedName());
1244+
}
12061245
}

0 commit comments

Comments
 (0)