Skip to content

feat(config): support config for AgentTool #1960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/google/adk/agents/common_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Common configuration classes for agent YAML configs."""
from __future__ import annotations

from typing import Any
from typing import List
from typing import Optional

from pydantic import BaseModel
from pydantic import ConfigDict

from ..utils.feature_decorator import working_in_progress


@working_in_progress("ArgumentConfig is not ready for use.")
class ArgumentConfig(BaseModel):
"""An argument passed to a function or a class's constructor."""

model_config = ConfigDict(extra="forbid")

name: Optional[str] = None
"""Optional. The argument name.

When the argument is for a positional argument, this can be omitted.
"""

value: Any
"""The argument value."""


@working_in_progress("CodeConfig is not ready for use.")
class CodeConfig(BaseModel):
"""Code reference config for a variable, a function, or a class.

This config is used for configuring callbacks and tools.
"""

model_config = ConfigDict(extra="forbid")

name: str
"""Required. The name of the variable, function, class, etc. in code.

Examples:

When used for tools,
- It can be ADK built-in tools, such as `google_search` and `AgentTool`.
- It can also be users' custom tools, e.g. my_library.my_tools.my_tool.

When used for callbacks, it refers to a function, e.g. `my_library.my_callbacks.my_callback`
"""

args: Optional[List[ArgumentConfig]] = None
"""Optional. The arguments for the code when `name` refers to a function or a
class's contructor.

Examples:
```
tools
- name: AgentTool
args:
- name: agent
value: search_agent.yaml
- name: skip_summarization
value: True
```
"""
33 changes: 32 additions & 1 deletion src/google/adk/agents/config_agent_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import importlib
import os
from pathlib import Path
from typing import Any

import yaml
Expand All @@ -24,6 +25,7 @@
from .agent_config import AgentConfig
from .base_agent import BaseAgent
from .base_agent import SubAgentConfig
from .common_configs import CodeConfig
from .llm_agent import LlmAgent
from .llm_agent import LlmAgentConfig
from .loop_agent import LoopAgent
Expand All @@ -39,7 +41,7 @@ def from_config(config_path: str) -> BaseAgent:
"""Build agent from a configfile path.

Args:
config: the path to a YAML config file.
config_path: the path to a YAML config file.

Returns:
The created agent instance.
Expand Down Expand Up @@ -138,3 +140,32 @@ def _resolve_sub_agent_code_reference(code: str) -> Any:
raise ValueError(f"Invalid code reference to a callable: {code}")

return obj


@working_in_progress("resolve_code_reference is not ready for use.")
def resolve_code_reference(code_config: CodeConfig) -> Any:
"""Resolve a code reference to actual Python object.

Args:
code_config: The code configuration (CodeConfig).

Returns:
The resolved Python object.

Raises:
ValueError: If the code reference cannot be resolved.
"""
if not code_config or not code_config.name:
raise ValueError("Invalid CodeConfig.")

module_path, obj_name = code_config.name.rsplit(".", 1)
module = importlib.import_module(module_path)
obj = getattr(module, obj_name)

if code_config.args and callable(obj):
kwargs = {arg.name: arg.value for arg in code_config.args if arg.name}
positional_args = [arg.value for arg in code_config.args if not arg.name]

return obj(*positional_args, **kwargs)
else:
return obj
71 changes: 71 additions & 0 deletions src/google/adk/agents/config_schemas/AgentConfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,61 @@
{
"$defs": {
"ArgumentConfig": {
"additionalProperties": false,
"description": "An argument passed to a function or a class's constructor.",
"properties": {
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Name"
},
"value": {
"title": "Value"
}
},
"required": [
"value"
],
"title": "ArgumentConfig",
"type": "object"
},
"CodeConfig": {
"additionalProperties": false,
"description": "Code reference config for a variable, a function, or a class.\n\nThis config is used for configuring callbacks and tools.",
"properties": {
"name": {
"title": "Name",
"type": "string"
},
"args": {
"anyOf": [
{
"items": {
"$ref": "#/$defs/ArgumentConfig"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"title": "Args"
}
},
"required": [
"name"
],
"title": "CodeConfig",
"type": "object"
},
"LlmAgentConfig": {
"additionalProperties": false,
"description": "The config for the YAML schema of a LlmAgent.",
Expand Down Expand Up @@ -97,6 +153,21 @@
],
"title": "Include Contents",
"type": "string"
},
"tools": {
"anyOf": [
{
"items": {
"$ref": "#/$defs/CodeConfig"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"title": "Tools"
}
},
"required": [
Expand Down
122 changes: 122 additions & 0 deletions src/google/adk/agents/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@

from __future__ import annotations

import importlib
import inspect
import logging
import os
from typing import Any
from typing import AsyncGenerator
from typing import Awaitable
from typing import Callable
from typing import Dict
from typing import List
from typing import Literal
from typing import Optional
from typing import Type
Expand Down Expand Up @@ -54,6 +57,7 @@
from .base_agent import BaseAgent
from .base_agent import BaseAgentConfig
from .callback_context import CallbackContext
from .common_configs import CodeConfig
from .invocation_context import InvocationContext
from .readonly_context import ReadonlyContext

Expand Down Expand Up @@ -520,6 +524,67 @@ def __validate_generate_content_config(
)
return generate_content_config

@classmethod
@working_in_progress('LlmAgent._resolve_tools is not ready for use.')
def _resolve_tools(
cls, tools_config: list[CodeConfig], config_abs_path: str
) -> list[Any]:
"""Resolve tools from configuration.

Args:
tools_config: List of tool configurations (CodeConfig objects).
config_abs_path: Absolute path to the config file.

Returns:
List of resolved tool objects.
"""
resolved_tools = []
for tool_config in tools_config:
if tool_config.name == 'AgentTool':
from ..tools.agent_tool import AgentTool
from .config_agent_utils import from_config

if tool_config.args:
kwargs = {arg.name: arg.value for arg in tool_config.args if arg.name}
positional_args = [
arg.value for arg in tool_config.args if not arg.name
]
if positional_args:
agent_path = positional_args[0]
if not os.path.isabs(agent_path):
folder_path = os.path.dirname(config_abs_path)
agent_path = os.path.join(folder_path, agent_path)
positional_args[0] = from_config(agent_path)
elif kwargs['agent']:
agent_path = kwargs['agent']
if not os.path.isabs(agent_path):
folder_path = os.path.dirname(config_abs_path)
agent_path = os.path.join(folder_path, agent_path)
kwargs['agent'] = from_config(agent_path)
else:
raise ValueError('Missing `agent` argument in AgentTool.')
resolved_tools.append(AgentTool(*positional_args, **kwargs))
else:
raise ValueError(
'AgentTool must have arguments. Please specify the arguments in'
' the config.'
)
elif '.' not in tool_config.name:
module = importlib.import_module('google.adk.tools')
obj = getattr(module, tool_config.name)
if isinstance(obj, ToolUnion):
resolved_tools.append(obj)
else:
raise ValueError(
f'Invalid tool name: {tool_config.name} is not a built-in tool.'
)
else:
from .config_agent_utils import resolve_code_reference

resolved_tools.append(resolve_code_reference(tool_config))

return resolved_tools

@classmethod
@override
@working_in_progress('LlmAgent.from_config is not ready for use.')
Expand All @@ -541,6 +606,8 @@ def from_config(
agent.include_contents = config.include_contents
if config.output_key:
agent.output_key = config.output_key
if config.tools:
agent.tools = cls._resolve_tools(config.tools, config_abs_path)
return agent


Expand Down Expand Up @@ -572,3 +639,58 @@ class LlmAgentConfig(BaseAgentConfig):

include_contents: Literal['default', 'none'] = 'default'
"""Optional. LlmAgent.include_contents."""

tools: Optional[list[CodeConfig]] = None
"""Optional. LlmAgent.tools.

Examples:

For ADK built-in tools in `google.adk.tools` package, they can be referenced
directly with the name:

```
tools:
- name: google_search
- name: load_memory
```

For user-defined tools, they can be referenced with fully qualified name:

```
tools:
- name: my_library.my_tools.my_tool
```

For tools that needs to be created via functions:

```
tools:
- name: my_library.my_tools.create_tool
args:
- name: param1
value: value1
- name: param2
value: value2
```

For more advanced tools, instead of specifying arguments in config, it's
recommended to define them in Python files and reference them. E.g.,

```
# tools.py
my_mcp_toolset = MCPToolset(
connection_params=StdioServerParameters(
command="npx",
args=["-y", "@notionhq/notion-mcp-server"],
env={"OPENAPI_MCP_HEADERS": NOTION_HEADERS},
)
)
```

Then, reference the toolset in config:

```
tools:
- name: tools.my_mcp_toolset
```
"""