Skip to content

Commit cb2a6ca

Browse files
Jacksunweicopybara-github
authored andcommitted
feat(config): Adds CustomAgentConfig to support user-defined agents in config
PiperOrigin-RevId: 786456046
1 parent c8f8b4a commit cb2a6ca

File tree

10 files changed

+332
-111
lines changed

10 files changed

+332
-111
lines changed

src/google/adk/agents/agent_config.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,46 @@
1414

1515
from __future__ import annotations
1616

17+
from typing import Any
1718
from typing import Union
1819

20+
from pydantic import Discriminator
1921
from pydantic import RootModel
2022

2123
from ..utils.feature_decorator import working_in_progress
24+
from .base_agent import BaseAgentConfig
2225
from .llm_agent import LlmAgentConfig
2326
from .loop_agent import LoopAgentConfig
2427
from .parallel_agent import ParallelAgentConfig
2528
from .sequential_agent import SequentialAgentConfig
2629

30+
2731
# A discriminated union of all possible agent configurations.
2832
ConfigsUnion = Union[
2933
LlmAgentConfig,
3034
LoopAgentConfig,
3135
ParallelAgentConfig,
3236
SequentialAgentConfig,
37+
BaseAgentConfig,
3338
]
3439

3540

41+
def agent_config_discriminator(v: Any):
42+
if isinstance(v, dict):
43+
agent_class = v.get("agent_class", "LlmAgent")
44+
if agent_class in [
45+
"LlmAgent",
46+
"LoopAgent",
47+
"ParallelAgent",
48+
"SequentialAgent",
49+
]:
50+
return agent_class
51+
else:
52+
return "BaseAgent"
53+
54+
raise ValueError(f"Invalid agent config: {v}")
55+
56+
3657
# Use a RootModel to represent the agent directly at the top level.
3758
# The `discriminator` is applied to the union within the RootModel.
3859
@working_in_progress("AgentConfig is not ready for use.")
@@ -43,4 +64,4 @@ class Config:
4364
# Pydantic v2 requires this for discriminated unions on RootModel
4465
# This tells the model to look at the 'agent_class' field of the input
4566
# data to decide which model from the `ConfigsUnion` to use.
46-
discriminator = "agent_class"
67+
discriminator = Discriminator(agent_config_discriminator)

src/google/adk/agents/base_agent.py

Lines changed: 1 addition & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
from typing import Callable
2222
from typing import Dict
2323
from typing import final
24-
from typing import List
25-
from typing import Literal
2624
from typing import Mapping
2725
from typing import Optional
2826
from typing import Type
@@ -36,14 +34,13 @@
3634
from pydantic import ConfigDict
3735
from pydantic import Field
3836
from pydantic import field_validator
39-
from pydantic import model_validator
4037
from typing_extensions import override
4138
from typing_extensions import TypeAlias
4239

4340
from ..events.event import Event
4441
from ..utils.feature_decorator import working_in_progress
42+
from .base_agent_config import BaseAgentConfig
4543
from .callback_context import CallbackContext
46-
from .common_configs import CodeConfig
4744

4845
if TYPE_CHECKING:
4946
from .invocation_context import InvocationContext
@@ -535,100 +532,3 @@ def from_config(
535532
config.after_agent_callbacks
536533
)
537534
return cls(**kwargs)
538-
539-
540-
class SubAgentConfig(BaseModel):
541-
"""The config for a sub-agent."""
542-
543-
model_config = ConfigDict(extra='forbid')
544-
545-
config: Optional[str] = None
546-
"""The YAML config file path of the sub-agent.
547-
548-
Only one of `config` or `code` can be set.
549-
550-
Example:
551-
552-
```
553-
sub_agents:
554-
- config: search_agent.yaml
555-
- config: my_library/my_custom_agent.yaml
556-
```
557-
"""
558-
559-
code: Optional[str] = None
560-
"""The agent instance defined in the code.
561-
562-
Only one of `config` or `code` can be set.
563-
564-
Example:
565-
566-
For the following agent defined in Python code:
567-
568-
```
569-
# my_library/custom_agents.py
570-
from google.adk.agents.llm_agent import LlmAgent
571-
572-
my_custom_agent = LlmAgent(
573-
name="my_custom_agent",
574-
instruction="You are a helpful custom agent.",
575-
model="gemini-2.0-flash",
576-
)
577-
```
578-
579-
The yaml config should be:
580-
581-
```
582-
sub_agents:
583-
- code: my_library.custom_agents.my_custom_agent
584-
```
585-
"""
586-
587-
@model_validator(mode='after')
588-
def validate_exactly_one_field(self):
589-
code_provided = self.code is not None
590-
config_provided = self.config is not None
591-
592-
if code_provided and config_provided:
593-
raise ValueError('Only one of code or config should be provided')
594-
if not code_provided and not config_provided:
595-
raise ValueError('Exactly one of code or config must be provided')
596-
597-
return self
598-
599-
600-
@working_in_progress('BaseAgentConfig is not ready for use.')
601-
class BaseAgentConfig(BaseModel):
602-
"""The config for the YAML schema of a BaseAgent.
603-
604-
Do not use this class directly. It's the base class for all agent configs.
605-
"""
606-
607-
model_config = ConfigDict(extra='forbid')
608-
609-
agent_class: Literal['BaseAgent'] = 'BaseAgent'
610-
"""Required. The class of the agent. The value is used to differentiate
611-
among different agent classes."""
612-
613-
name: str
614-
"""Required. The name of the agent."""
615-
616-
description: str = ''
617-
"""Optional. The description of the agent."""
618-
619-
sub_agents: Optional[List[SubAgentConfig]] = None
620-
"""Optional. The sub-agents of the agent."""
621-
622-
before_agent_callbacks: Optional[List[CodeConfig]] = None
623-
"""Optional. The before_agent_callbacks of the agent.
624-
625-
Example:
626-
627-
```
628-
before_agent_callbacks:
629-
- name: my_library.security_callbacks.before_agent_callback
630-
```
631-
"""
632-
633-
after_agent_callbacks: Optional[List[CodeConfig]] = None
634-
"""Optional. The after_agent_callbacks of the agent."""
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import inspect
18+
from typing import Any
19+
from typing import AsyncGenerator
20+
from typing import Awaitable
21+
from typing import Callable
22+
from typing import Dict
23+
from typing import final
24+
from typing import List
25+
from typing import Literal
26+
from typing import Mapping
27+
from typing import Optional
28+
from typing import Type
29+
from typing import TYPE_CHECKING
30+
from typing import TypeVar
31+
from typing import Union
32+
33+
from google.genai import types
34+
from opentelemetry import trace
35+
from pydantic import BaseModel
36+
from pydantic import ConfigDict
37+
from pydantic import Field
38+
from pydantic import field_validator
39+
from pydantic import model_validator
40+
from typing_extensions import override
41+
from typing_extensions import TypeAlias
42+
43+
from ..events.event import Event
44+
from ..utils.feature_decorator import working_in_progress
45+
from .callback_context import CallbackContext
46+
from .common_configs import CodeConfig
47+
48+
if TYPE_CHECKING:
49+
from .invocation_context import InvocationContext
50+
51+
52+
TBaseAgentConfig = TypeVar('TBaseAgentConfig', bound='BaseAgentConfig')
53+
54+
55+
class SubAgentConfig(BaseModel):
56+
"""The config for a sub-agent."""
57+
58+
model_config = ConfigDict(extra='forbid')
59+
60+
config: Optional[str] = None
61+
"""The YAML config file path of the sub-agent.
62+
63+
Only one of `config` or `code` can be set.
64+
65+
Example:
66+
67+
```
68+
sub_agents:
69+
- config: search_agent.yaml
70+
- config: my_library/my_custom_agent.yaml
71+
```
72+
"""
73+
74+
code: Optional[str] = None
75+
"""The agent instance defined in the code.
76+
77+
Only one of `config` or `code` can be set.
78+
79+
Example:
80+
81+
For the following agent defined in Python code:
82+
83+
```
84+
# my_library/custom_agents.py
85+
from google.adk.agents.llm_agent import LlmAgent
86+
87+
my_custom_agent = LlmAgent(
88+
name="my_custom_agent",
89+
instruction="You are a helpful custom agent.",
90+
model="gemini-2.0-flash",
91+
)
92+
```
93+
94+
The yaml config should be:
95+
96+
```
97+
sub_agents:
98+
- code: my_library.custom_agents.my_custom_agent
99+
```
100+
"""
101+
102+
@model_validator(mode='after')
103+
def validate_exactly_one_field(self):
104+
code_provided = self.code is not None
105+
config_provided = self.config is not None
106+
107+
if code_provided and config_provided:
108+
raise ValueError('Only one of code or config should be provided')
109+
if not code_provided and not config_provided:
110+
raise ValueError('Exactly one of code or config must be provided')
111+
112+
return self
113+
114+
115+
@working_in_progress('BaseAgentConfig is not ready for use.')
116+
class BaseAgentConfig(BaseModel):
117+
"""The config for the YAML schema of a BaseAgent.
118+
119+
Do not use this class directly. It's the base class for all agent configs.
120+
"""
121+
122+
model_config = ConfigDict(
123+
extra='forbid',
124+
)
125+
126+
agent_class: Union[Literal['BaseAgent'], str] = 'BaseAgent'
127+
"""Required. The class of the agent. The value is used to differentiate
128+
among different agent classes."""
129+
130+
name: str
131+
"""Required. The name of the agent."""
132+
133+
description: str = ''
134+
"""Optional. The description of the agent."""
135+
136+
sub_agents: Optional[List[SubAgentConfig]] = None
137+
"""Optional. The sub-agents of the agent."""
138+
139+
before_agent_callbacks: Optional[List[CodeConfig]] = None
140+
"""Optional. The before_agent_callbacks of the agent.
141+
142+
Example:
143+
144+
```
145+
before_agent_callbacks:
146+
- name: my_library.security_callbacks.before_agent_callback
147+
```
148+
"""
149+
150+
after_agent_callbacks: Optional[List[CodeConfig]] = None
151+
"""Optional. The after_agent_callbacks of the agent."""
152+
153+
def to_agent_config(
154+
self, custom_agent_config_cls: Type[TBaseAgentConfig]
155+
) -> TBaseAgentConfig:
156+
"""Converts this config to the concrete agent config type.
157+
158+
NOTE: this is for ADK framework use only.
159+
"""
160+
return custom_agent_config_cls.model_validate(self.model_dump())

src/google/adk/agents/config_agent_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from ..utils.feature_decorator import working_in_progress
2525
from .agent_config import AgentConfig
2626
from .base_agent import BaseAgent
27-
from .base_agent import SubAgentConfig
27+
from .base_agent_config import SubAgentConfig
2828
from .common_configs import CodeConfig
2929
from .llm_agent import LlmAgent
3030
from .llm_agent import LlmAgentConfig

src/google/adk/agents/llm_agent.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
from google.genai import types
3131
from pydantic import BaseModel
32+
from pydantic import ConfigDict
3233
from pydantic import Field
3334
from pydantic import field_validator
3435
from pydantic import model_validator
@@ -53,7 +54,7 @@
5354
from ..tools.tool_context import ToolContext
5455
from ..utils.feature_decorator import working_in_progress
5556
from .base_agent import BaseAgent
56-
from .base_agent import BaseAgentConfig
57+
from .base_agent_config import BaseAgentConfig
5758
from .callback_context import CallbackContext
5859
from .common_configs import CodeConfig
5960
from .invocation_context import InvocationContext
@@ -607,6 +608,10 @@ def from_config(
607608
class LlmAgentConfig(BaseAgentConfig):
608609
"""The config for the YAML schema of a LlmAgent."""
609610

611+
model_config = ConfigDict(
612+
extra='forbid',
613+
)
614+
610615
agent_class: Literal['LlmAgent', ''] = 'LlmAgent'
611616
"""The value is used to uniquely identify the LlmAgent class. If it is
612617
empty, it is by default an LlmAgent."""

0 commit comments

Comments
 (0)