diff --git a/src/google/adk/agents/base_agent.py b/src/google/adk/agents/base_agent.py index 645256fd0..7ed1f93de 100644 --- a/src/google/adk/agents/base_agent.py +++ b/src/google/adk/agents/base_agent.py @@ -21,6 +21,7 @@ from typing import Callable from typing import Dict from typing import final +from typing import List from typing import Literal from typing import Mapping from typing import Optional @@ -35,6 +36,7 @@ from pydantic import ConfigDict from pydantic import Field from pydantic import field_validator +from pydantic import model_validator from typing_extensions import override from typing_extensions import TypeAlias @@ -491,6 +493,7 @@ def __set_parent_agent_for_sub_agents(self) -> BaseAgent: def from_config( cls: Type[SelfAgent], config: BaseAgentConfig, + config_abs_path: str, ) -> SelfAgent: """Creates an agent from a config. @@ -506,13 +509,83 @@ def from_config( Returns: The created agent. """ + from .config_agent_utils import build_sub_agent + kwargs: Dict[str, Any] = { 'name': config.name, 'description': config.description, } + if config.sub_agents: + sub_agents = [] + for sub_agent_config in config.sub_agents: + sub_agent = build_sub_agent( + sub_agent_config, config_abs_path.rsplit('/', 1)[0] + ) + sub_agents.append(sub_agent) + kwargs['sub_agents'] = sub_agents return cls(**kwargs) +class SubAgentConfig(BaseModel): + """The config for a sub-agent.""" + + model_config = ConfigDict(extra='forbid') + + config: Optional[str] = None + """The YAML config file path of the sub-agent. + + Only one of `config` or `code` can be set. + + Example: + + ``` + sub_agents: + - config: search_agent.yaml + - config: my_library/my_custom_agent.yaml + ``` + """ + + code: Optional[str] = None + """The agent instance defined in the code. + + Only one of `config` or `code` can be set. + + Example: + + For the following agent defined in Python code: + + ``` + # my_library/custom_agents.py + from google.adk.agents import LlmAgent + + my_custom_agent = LlmAgent( + name="my_custom_agent", + instruction="You are a helpful custom agent.", + model="gemini-2.0-flash", + ) + ``` + + The yaml config should be: + + ``` + sub_agents: + - code: my_library.custom_agents.my_custom_agent + ``` + """ + + @model_validator(mode='after') + def validate_exactly_one_field(self): + code_provided = self.code is not None + config_provided = self.config is not None + + if code_provided and config_provided: + raise ValueError('Only one of code or config should be provided') + if not code_provided and not config_provided: + raise ValueError('Exactly one of code or config must be provided') + + return self + + @working_in_progress('BaseAgentConfig is not ready for use.') class BaseAgentConfig(BaseModel): """The config for the YAML schema of a BaseAgent. @@ -531,3 +604,6 @@ class BaseAgentConfig(BaseModel): description: str = '' """Optional. The description of the agent.""" + + sub_agents: Optional[List[SubAgentConfig]] = None + """Optional. The sub-agents of the agent.""" diff --git a/src/google/adk/agents/config_agent_utils.py b/src/google/adk/agents/config_agent_utils.py index a9609b684..375482875 100644 --- a/src/google/adk/agents/config_agent_utils.py +++ b/src/google/adk/agents/config_agent_utils.py @@ -14,14 +14,16 @@ from __future__ import annotations +import importlib import os -from pathlib import Path +from typing import Any import yaml from ..utils.feature_decorator import working_in_progress from .agent_config import AgentConfig from .base_agent import BaseAgent +from .base_agent import SubAgentConfig from .llm_agent import LlmAgent from .llm_agent import LlmAgentConfig from .loop_agent import LoopAgent @@ -51,13 +53,13 @@ def from_config(config_path: str) -> BaseAgent: config = _load_config_from_path(abs_path) if isinstance(config.root, LlmAgentConfig): - return LlmAgent.from_config(config.root) + return LlmAgent.from_config(config.root, abs_path) elif isinstance(config.root, LoopAgentConfig): - return LoopAgent.from_config(config.root) + return LoopAgent.from_config(config.root, abs_path) elif isinstance(config.root, ParallelAgentConfig): - return ParallelAgent.from_config(config.root) + return ParallelAgent.from_config(config.root, abs_path) elif isinstance(config.root, SequentialAgentConfig): - return SequentialAgent.from_config(config.root) + return SequentialAgent.from_config(config.root, abs_path) else: raise ValueError("Unsupported config type") @@ -77,12 +79,62 @@ def _load_config_from_path(config_path: str) -> AgentConfig: FileNotFoundError: If config file doesn't exist. ValidationError: If config file's content is invalid YAML. """ - config_path = Path(config_path) - - if not config_path.exists(): + if not os.path.exists(config_path): raise FileNotFoundError(f"Config file not found: {config_path}") with open(config_path, "r", encoding="utf-8") as f: config_data = yaml.safe_load(f) return AgentConfig.model_validate(config_data) + + +@working_in_progress("build_sub_agent is not ready for use.") +def build_sub_agent( + sub_config: SubAgentConfig, parent_agent_folder_path: str +) -> BaseAgent: + """Build a sub-agent from configuration. + + Args: + sub_config: The sub-agent configuration (SubAgentConfig). + parent_agent_folder_path: The folder path to the parent agent's YAML config. + + Returns: + The created sub-agent instance. + """ + if sub_config.config: + if os.path.isabs(sub_config.config): + return from_config(sub_config.config) + else: + return from_config( + os.path.join(parent_agent_folder_path, sub_config.config) + ) + elif sub_config.code: + return _resolve_sub_agent_code_reference(sub_config.code) + else: + raise ValueError("SubAgentConfig must have either 'code' or 'config'") + + +@working_in_progress("_resolve_sub_agent_code_reference is not ready for use.") +def _resolve_sub_agent_code_reference(code: str) -> Any: + """Resolve a code reference to an actual agent object. + + Args: + code: The code reference to the sub-agent. + + Returns: + The resolved agent object. + + Raises: + ValueError: If the code reference cannot be resolved. + """ + if "." not in code: + raise ValueError(f"Invalid code reference: {code}") + + module_path, obj_name = code.rsplit(".", 1) + module = importlib.import_module(module_path) + obj = getattr(module, obj_name) + + if callable(obj): + raise ValueError(f"Invalid code reference to a callable: {code}") + + return obj diff --git a/src/google/adk/agents/config_schemas/AgentConfig.json b/src/google/adk/agents/config_schemas/AgentConfig.json index b353ba11f..98be5dfd8 100644 --- a/src/google/adk/agents/config_schemas/AgentConfig.json +++ b/src/google/adk/agents/config_schemas/AgentConfig.json @@ -22,6 +22,21 @@ "title": "Description", "type": "string" }, + "sub_agents": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SubAgentConfig" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sub Agents" + }, "model": { "anyOf": [ { @@ -89,6 +104,21 @@ "title": "Description", "type": "string" }, + "sub_agents": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SubAgentConfig" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sub Agents" + }, "max_iterations": { "anyOf": [ { @@ -126,6 +156,21 @@ "default": "", "title": "Description", "type": "string" + }, + "sub_agents": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SubAgentConfig" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sub Agents" } }, "required": [ @@ -152,6 +197,21 @@ "default": "", "title": "Description", "type": "string" + }, + "sub_agents": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SubAgentConfig" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sub Agents" } }, "required": [ @@ -159,6 +219,38 @@ ], "title": "SequentialAgentConfig", "type": "object" + }, + "SubAgentConfig": { + "additionalProperties": false, + "description": "The config for a sub-agent.", + "properties": { + "config": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Code" + } + }, + "title": "SubAgentConfig", + "type": "object" } }, "anyOf": [ diff --git a/src/google/adk/agents/llm_agent.py b/src/google/adk/agents/llm_agent.py index 6c5e109aa..0926518c5 100644 --- a/src/google/adk/agents/llm_agent.py +++ b/src/google/adk/agents/llm_agent.py @@ -526,8 +526,9 @@ def __validate_generate_content_config( def from_config( cls: Type[LlmAgent], config: LlmAgentConfig, + config_abs_path: str, ) -> LlmAgent: - agent = super().from_config(config) + agent = super().from_config(config, config_abs_path) if config.model: agent.model = config.model if config.instruction: diff --git a/src/google/adk/agents/loop_agent.py b/src/google/adk/agents/loop_agent.py index 7a2c22308..72157a830 100644 --- a/src/google/adk/agents/loop_agent.py +++ b/src/google/adk/agents/loop_agent.py @@ -73,8 +73,9 @@ async def _run_live_impl( def from_config( cls: Type[LoopAgent], config: LoopAgentConfig, + config_abs_path: str, ) -> LoopAgent: - agent = super().from_config(config) + agent = super().from_config(config, config_abs_path) if config.max_iterations: agent.max_iterations = config.max_iterations return agent diff --git a/src/google/adk/agents/parallel_agent.py b/src/google/adk/agents/parallel_agent.py index 10b573bbe..36034056c 100644 --- a/src/google/adk/agents/parallel_agent.py +++ b/src/google/adk/agents/parallel_agent.py @@ -122,8 +122,9 @@ async def _run_live_impl( def from_config( cls: Type[ParallelAgent], config: ParallelAgentConfig, + config_abs_path: str, ) -> ParallelAgent: - return super().from_config(config) + return super().from_config(config, config_abs_path) @working_in_progress('ParallelAgentConfig is not ready for use.') diff --git a/src/google/adk/agents/sequential_agent.py b/src/google/adk/agents/sequential_agent.py index e094f8aed..51dff22ce 100644 --- a/src/google/adk/agents/sequential_agent.py +++ b/src/google/adk/agents/sequential_agent.py @@ -85,8 +85,9 @@ def task_completed(): def from_config( cls: Type[SequentialAgent], config: SequentialAgentConfig, + config_abs_path: str, ) -> SequentialAgent: - return super().from_config(config) + return super().from_config(config, config_abs_path) @working_in_progress('SequentialAgentConfig is not ready for use.')