feat(config): add APIs for building config agents

Including basic fields in configs, the from_config() methods and the JSON schema. Other fields will be added in following PRs.

PiperOrigin-RevId: 781660569
This commit is contained in:
Liang Wu
2025-07-10 13:32:28 -07:00
committed by Copybara-Service
parent fb2415395f
commit ca396a3ab1
5 changed files with 251 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
# 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.
from __future__ import annotations
from typing import Union
from pydantic import RootModel
from ..utils.feature_decorator import working_in_progress
from .llm_agent import LlmAgentConfig
from .loop_agent import LoopAgentConfig
# A discriminated union of all possible agent configurations.
ConfigsUnion = Union[
LlmAgentConfig,
LoopAgentConfig,
]
# Use a RootModel to represent the agent directly at the top level.
# The `discriminator` is applied to the union within the RootModel.
@working_in_progress("AgentConfig is not ready for use.")
class AgentConfig(RootModel[ConfigsUnion]):
"""The config for the YAML schema to create an agent."""
class Config:
# Pydantic v2 requires this for discriminated unions on RootModel
# This tells the model to look at the 'agent_class' field of the input
# data to decide which model from the `ConfigsUnion` to use.
discriminator = "agent_class"
+50
View File
@@ -19,9 +19,12 @@ from typing import Any
from typing import AsyncGenerator
from typing import Awaitable
from typing import Callable
from typing import Dict
from typing import final
from typing import Literal
from typing import Mapping
from typing import Optional
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@@ -36,6 +39,7 @@ from typing_extensions import override
from typing_extensions import TypeAlias
from ..events.event import Event
from ..utils.feature_decorator import working_in_progress
from .callback_context import CallbackContext
if TYPE_CHECKING:
@@ -439,3 +443,49 @@ class BaseAgent(BaseModel):
)
sub_agent.parent_agent = self
return self
@classmethod
@working_in_progress('BaseAgent.from_config is not ready for use.')
def from_config(
cls: Type[SelfAgent],
config: BaseAgentConfig,
) -> SelfAgent:
"""Creates an agent from a config.
This method converts fields in a config to the corresponding
fields in an agent.
Child classes should re-implement this method to support loading from their
custom config types.
Args:
config: The config to create the agent from.
Returns:
The created agent.
"""
kwargs: Dict[str, Any] = {
'name': config.name,
'description': config.description,
}
return cls(**kwargs)
@working_in_progress('BaseAgentConfig is not ready for use.')
class BaseAgentConfig(BaseModel):
"""The config for the YAML schema of a BaseAgent.
Do not use this class directly. It's the base class for all agent configs.
"""
model_config = ConfigDict(extra='forbid')
agent_class: Literal['BaseAgent'] = 'BaseAgent'
"""Required. The class of the agent. The value is used to differentiate
among different agent classes."""
name: str
"""Required. The name of the agent."""
description: str = ''
"""Optional. The description of the agent."""
@@ -0,0 +1,98 @@
{
"$defs": {
"LlmAgentConfig": {
"additionalProperties": false,
"description": "The config for the YAML schema of a LlmAgent.",
"properties": {
"agent_class": {
"default": "LlmAgent",
"enum": [
"LlmAgent",
""
],
"title": "Agent Class",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"description": {
"default": "",
"title": "Description",
"type": "string"
},
"model": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Model"
},
"instruction": {
"title": "Instruction",
"type": "string"
}
},
"required": [
"name",
"instruction"
],
"title": "LlmAgentConfig",
"type": "object"
},
"LoopAgentConfig": {
"additionalProperties": false,
"description": "The config for the YAML schema of a LoopAgent.",
"properties": {
"agent_class": {
"const": "LoopAgent",
"default": "LoopAgent",
"title": "Agent Class",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"description": {
"default": "",
"title": "Description",
"type": "string"
},
"max_iterations": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Max Iterations"
}
},
"required": [
"name"
],
"title": "LoopAgentConfig",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/LlmAgentConfig"
},
{
"$ref": "#/$defs/LoopAgentConfig"
}
],
"description": "The config for the YAML schema to create an agent.",
"title": "AgentConfig"
}
+33
View File
@@ -20,8 +20,10 @@ from typing import Any
from typing import AsyncGenerator
from typing import Awaitable
from typing import Callable
from typing import Dict
from typing import Literal
from typing import Optional
from typing import Type
from typing import Union
from google.genai import types
@@ -48,7 +50,9 @@ from ..tools.base_tool import BaseTool
from ..tools.base_toolset import BaseToolset
from ..tools.function_tool import FunctionTool
from ..tools.tool_context import ToolContext
from ..utils.feature_decorator import working_in_progress
from .base_agent import BaseAgent
from .base_agent import BaseAgentConfig
from .callback_context import CallbackContext
from .invocation_context import InvocationContext
from .readonly_context import ReadonlyContext
@@ -516,5 +520,34 @@ class LlmAgent(BaseAgent):
)
return generate_content_config
@classmethod
@override
@working_in_progress('LlmAgent.from_config is not ready for use.')
def from_config(
cls: Type[LlmAgent],
config: LlmAgentConfig,
) -> LlmAgent:
agent = super().from_config(config)
if config.model:
agent.model = config.model
if config.instruction:
agent.instruction = config.instruction
return agent
Agent: TypeAlias = LlmAgent
class LlmAgentConfig(BaseAgentConfig):
"""The config for the YAML schema of a LlmAgent."""
agent_class: Literal['LlmAgent', ''] = 'LlmAgent'
"""The value is used to uniquely identify the LlmAgent class. If it is
empty, it is by default an LlmAgent."""
model: Optional[str] = None
"""Optional. LlmAgent.model. If not set, the model will be inherited from
the ancestor."""
instruction: str
"""Required. LlmAgent.instruction."""
+28
View File
@@ -16,14 +16,20 @@
from __future__ import annotations
from typing import Any
from typing import AsyncGenerator
from typing import Dict
from typing import Literal
from typing import Optional
from typing import Type
from typing_extensions import override
from ..agents.invocation_context import InvocationContext
from ..events.event import Event
from ..utils.feature_decorator import working_in_progress
from .base_agent import BaseAgent
from .base_agent import BaseAgentConfig
class LoopAgent(BaseAgent):
@@ -60,3 +66,25 @@ class LoopAgent(BaseAgent):
) -> AsyncGenerator[Event, None]:
raise NotImplementedError('This is not supported yet for LoopAgent.')
yield # AsyncGenerator requires having at least one yield statement
@classmethod
@override
@working_in_progress('LoopAgent.from_config is not ready for use.')
def from_config(
cls: Type[LoopAgent],
config: LoopAgentConfig,
) -> LoopAgent:
agent = super().from_config(config)
if config.max_iterations:
agent.max_iterations = config.max_iterations
return agent
@working_in_progress('LoopAgentConfig is not ready for use.')
class LoopAgentConfig(BaseAgentConfig):
"""The config for the YAML schema of a LoopAgent."""
agent_class: Literal['LoopAgent'] = 'LoopAgent'
max_iterations: Optional[int] = None
"""Optional. LoopAgent.max_iterations."""