Skip to content

Commit b2ef9a0

Browse files
wuliang229copybara-github
authored andcommitted
feat(config): support sub_agents in BaseAgentConfig
Currently only support path to YAML or code reference to agent instance. PiperOrigin-RevId: 782157110
1 parent 134ec0d commit b2ef9a0

File tree

7 files changed

+236
-12
lines changed

7 files changed

+236
-12
lines changed

src/google/adk/agents/base_agent.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from typing import Callable
2222
from typing import Dict
2323
from typing import final
24+
from typing import List
2425
from typing import Literal
2526
from typing import Mapping
2627
from typing import Optional
@@ -35,6 +36,7 @@
3536
from pydantic import ConfigDict
3637
from pydantic import Field
3738
from pydantic import field_validator
39+
from pydantic import model_validator
3840
from typing_extensions import override
3941
from typing_extensions import TypeAlias
4042

@@ -491,6 +493,7 @@ def __set_parent_agent_for_sub_agents(self) -> BaseAgent:
491493
def from_config(
492494
cls: Type[SelfAgent],
493495
config: BaseAgentConfig,
496+
config_abs_path: str,
494497
) -> SelfAgent:
495498
"""Creates an agent from a config.
496499
@@ -506,13 +509,83 @@ def from_config(
506509
Returns:
507510
The created agent.
508511
"""
512+
from .config_agent_utils import build_sub_agent
513+
509514
kwargs: Dict[str, Any] = {
510515
'name': config.name,
511516
'description': config.description,
512517
}
518+
if config.sub_agents:
519+
sub_agents = []
520+
for sub_agent_config in config.sub_agents:
521+
sub_agent = build_sub_agent(
522+
sub_agent_config, config_abs_path.rsplit('/', 1)[0]
523+
)
524+
sub_agents.append(sub_agent)
525+
kwargs['sub_agents'] = sub_agents
513526
return cls(**kwargs)
514527

515528

529+
class SubAgentConfig(BaseModel):
530+
"""The config for a sub-agent."""
531+
532+
model_config = ConfigDict(extra='forbid')
533+
534+
config: Optional[str] = None
535+
"""The YAML config file path of the sub-agent.
536+
537+
Only one of `config` or `code` can be set.
538+
539+
Example:
540+
541+
```
542+
sub_agents:
543+
- config: search_agent.yaml
544+
- config: my_library/my_custom_agent.yaml
545+
```
546+
"""
547+
548+
code: Optional[str] = None
549+
"""The agent instance defined in the code.
550+
551+
Only one of `config` or `code` can be set.
552+
553+
Example:
554+
555+
For the following agent defined in Python code:
556+
557+
```
558+
# my_library/custom_agents.py
559+
from google.adk.agents import LlmAgent
560+
561+
my_custom_agent = LlmAgent(
562+
name="my_custom_agent",
563+
instruction="You are a helpful custom agent.",
564+
model="gemini-2.0-flash",
565+
)
566+
```
567+
568+
The yaml config should be:
569+
570+
```
571+
sub_agents:
572+
- code: my_library.custom_agents.my_custom_agent
573+
```
574+
"""
575+
576+
@model_validator(mode='after')
577+
def validate_exactly_one_field(self):
578+
code_provided = self.code is not None
579+
config_provided = self.config is not None
580+
581+
if code_provided and config_provided:
582+
raise ValueError('Only one of code or config should be provided')
583+
if not code_provided and not config_provided:
584+
raise ValueError('Exactly one of code or config must be provided')
585+
586+
return self
587+
588+
516589
@working_in_progress('BaseAgentConfig is not ready for use.')
517590
class BaseAgentConfig(BaseModel):
518591
"""The config for the YAML schema of a BaseAgent.
@@ -531,3 +604,6 @@ class BaseAgentConfig(BaseModel):
531604

532605
description: str = ''
533606
"""Optional. The description of the agent."""
607+
608+
sub_agents: Optional[List[SubAgentConfig]] = None
609+
"""Optional. The sub-agents of the agent."""

src/google/adk/agents/config_agent_utils.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414

1515
from __future__ import annotations
1616

17+
import importlib
1718
import os
18-
from pathlib import Path
19+
from typing import Any
1920

2021
import yaml
2122

2223
from ..utils.feature_decorator import working_in_progress
2324
from .agent_config import AgentConfig
2425
from .base_agent import BaseAgent
26+
from .base_agent import SubAgentConfig
2527
from .llm_agent import LlmAgent
2628
from .llm_agent import LlmAgentConfig
2729
from .loop_agent import LoopAgent
@@ -51,13 +53,13 @@ def from_config(config_path: str) -> BaseAgent:
5153
config = _load_config_from_path(abs_path)
5254

5355
if isinstance(config.root, LlmAgentConfig):
54-
return LlmAgent.from_config(config.root)
56+
return LlmAgent.from_config(config.root, abs_path)
5557
elif isinstance(config.root, LoopAgentConfig):
56-
return LoopAgent.from_config(config.root)
58+
return LoopAgent.from_config(config.root, abs_path)
5759
elif isinstance(config.root, ParallelAgentConfig):
58-
return ParallelAgent.from_config(config.root)
60+
return ParallelAgent.from_config(config.root, abs_path)
5961
elif isinstance(config.root, SequentialAgentConfig):
60-
return SequentialAgent.from_config(config.root)
62+
return SequentialAgent.from_config(config.root, abs_path)
6163
else:
6264
raise ValueError("Unsupported config type")
6365

@@ -77,12 +79,62 @@ def _load_config_from_path(config_path: str) -> AgentConfig:
7779
FileNotFoundError: If config file doesn't exist.
7880
ValidationError: If config file's content is invalid YAML.
7981
"""
80-
config_path = Path(config_path)
81-
82-
if not config_path.exists():
82+
if not os.path.exists(config_path):
8383
raise FileNotFoundError(f"Config file not found: {config_path}")
8484

8585
with open(config_path, "r", encoding="utf-8") as f:
8686
config_data = yaml.safe_load(f)
8787

8888
return AgentConfig.model_validate(config_data)
89+
90+
91+
@working_in_progress("build_sub_agent is not ready for use.")
92+
def build_sub_agent(
93+
sub_config: SubAgentConfig, parent_agent_folder_path: str
94+
) -> BaseAgent:
95+
"""Build a sub-agent from configuration.
96+
97+
Args:
98+
sub_config: The sub-agent configuration (SubAgentConfig).
99+
parent_agent_folder_path: The folder path to the parent agent's YAML config.
100+
101+
Returns:
102+
The created sub-agent instance.
103+
"""
104+
if sub_config.config:
105+
if os.path.isabs(sub_config.config):
106+
return from_config(sub_config.config)
107+
else:
108+
return from_config(
109+
os.path.join(parent_agent_folder_path, sub_config.config)
110+
)
111+
elif sub_config.code:
112+
return _resolve_sub_agent_code_reference(sub_config.code)
113+
else:
114+
raise ValueError("SubAgentConfig must have either 'code' or 'config'")
115+
116+
117+
@working_in_progress("_resolve_sub_agent_code_reference is not ready for use.")
118+
def _resolve_sub_agent_code_reference(code: str) -> Any:
119+
"""Resolve a code reference to an actual agent object.
120+
121+
Args:
122+
code: The code reference to the sub-agent.
123+
124+
Returns:
125+
The resolved agent object.
126+
127+
Raises:
128+
ValueError: If the code reference cannot be resolved.
129+
"""
130+
if "." not in code:
131+
raise ValueError(f"Invalid code reference: {code}")
132+
133+
module_path, obj_name = code.rsplit(".", 1)
134+
module = importlib.import_module(module_path)
135+
obj = getattr(module, obj_name)
136+
137+
if callable(obj):
138+
raise ValueError(f"Invalid code reference to a callable: {code}")
139+
140+
return obj

src/google/adk/agents/config_schemas/AgentConfig.json

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@
2222
"title": "Description",
2323
"type": "string"
2424
},
25+
"sub_agents": {
26+
"anyOf": [
27+
{
28+
"items": {
29+
"$ref": "#/$defs/SubAgentConfig"
30+
},
31+
"type": "array"
32+
},
33+
{
34+
"type": "null"
35+
}
36+
],
37+
"default": null,
38+
"title": "Sub Agents"
39+
},
2540
"model": {
2641
"anyOf": [
2742
{
@@ -89,6 +104,21 @@
89104
"title": "Description",
90105
"type": "string"
91106
},
107+
"sub_agents": {
108+
"anyOf": [
109+
{
110+
"items": {
111+
"$ref": "#/$defs/SubAgentConfig"
112+
},
113+
"type": "array"
114+
},
115+
{
116+
"type": "null"
117+
}
118+
],
119+
"default": null,
120+
"title": "Sub Agents"
121+
},
92122
"max_iterations": {
93123
"anyOf": [
94124
{
@@ -126,6 +156,21 @@
126156
"default": "",
127157
"title": "Description",
128158
"type": "string"
159+
},
160+
"sub_agents": {
161+
"anyOf": [
162+
{
163+
"items": {
164+
"$ref": "#/$defs/SubAgentConfig"
165+
},
166+
"type": "array"
167+
},
168+
{
169+
"type": "null"
170+
}
171+
],
172+
"default": null,
173+
"title": "Sub Agents"
129174
}
130175
},
131176
"required": [
@@ -152,13 +197,60 @@
152197
"default": "",
153198
"title": "Description",
154199
"type": "string"
200+
},
201+
"sub_agents": {
202+
"anyOf": [
203+
{
204+
"items": {
205+
"$ref": "#/$defs/SubAgentConfig"
206+
},
207+
"type": "array"
208+
},
209+
{
210+
"type": "null"
211+
}
212+
],
213+
"default": null,
214+
"title": "Sub Agents"
155215
}
156216
},
157217
"required": [
158218
"name"
159219
],
160220
"title": "SequentialAgentConfig",
161221
"type": "object"
222+
},
223+
"SubAgentConfig": {
224+
"additionalProperties": false,
225+
"description": "The config for a sub-agent.",
226+
"properties": {
227+
"config": {
228+
"anyOf": [
229+
{
230+
"type": "string"
231+
},
232+
{
233+
"type": "null"
234+
}
235+
],
236+
"default": null,
237+
"title": "Config"
238+
},
239+
"code": {
240+
"anyOf": [
241+
{
242+
"type": "string"
243+
},
244+
{
245+
"type": "null"
246+
}
247+
],
248+
"default": null,
249+
"title": "Code"
250+
}
251+
},
252+
"title": "SubAgentConfig",
253+
"type": "object"
162254
}
163255
},
164256
"anyOf": [

src/google/adk/agents/llm_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,9 @@ def __validate_generate_content_config(
526526
def from_config(
527527
cls: Type[LlmAgent],
528528
config: LlmAgentConfig,
529+
config_abs_path: str,
529530
) -> LlmAgent:
530-
agent = super().from_config(config)
531+
agent = super().from_config(config, config_abs_path)
531532
if config.model:
532533
agent.model = config.model
533534
if config.instruction:

src/google/adk/agents/loop_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ async def _run_live_impl(
7373
def from_config(
7474
cls: Type[LoopAgent],
7575
config: LoopAgentConfig,
76+
config_abs_path: str,
7677
) -> LoopAgent:
77-
agent = super().from_config(config)
78+
agent = super().from_config(config, config_abs_path)
7879
if config.max_iterations:
7980
agent.max_iterations = config.max_iterations
8081
return agent

src/google/adk/agents/parallel_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,9 @@ async def _run_live_impl(
122122
def from_config(
123123
cls: Type[ParallelAgent],
124124
config: ParallelAgentConfig,
125+
config_abs_path: str,
125126
) -> ParallelAgent:
126-
return super().from_config(config)
127+
return super().from_config(config, config_abs_path)
127128

128129

129130
@working_in_progress('ParallelAgentConfig is not ready for use.')

src/google/adk/agents/sequential_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ def task_completed():
8585
def from_config(
8686
cls: Type[SequentialAgent],
8787
config: SequentialAgentConfig,
88+
config_abs_path: str,
8889
) -> SequentialAgent:
89-
return super().from_config(config)
90+
return super().from_config(config, config_abs_path)
9091

9192

9293
@working_in_progress('SequentialAgentConfig is not ready for use.')

0 commit comments

Comments
 (0)