Skip to content

Commit 483c5ba

Browse files
google-genai-botcopybara-github
authored andcommitted
ADK changes
PiperOrigin-RevId: 866173091
1 parent 0b9cbd2 commit 483c5ba

14 files changed

+1146
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2026 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 google.adk.tools.agent_simulator.agent_simulator_factory import AgentSimulatorFactory
16+
17+
__all__ = ["AgentSimulator"]
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright 2026 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 enum
18+
from typing import Any
19+
from typing import Dict
20+
from typing import List
21+
from typing import Optional
22+
23+
from google.genai import types as genai_types
24+
from pydantic import BaseModel
25+
from pydantic import Field
26+
from pydantic import field_validator
27+
from pydantic import model_validator
28+
from pydantic_core import ValidationError
29+
30+
31+
class InjectedError(BaseModel):
32+
"""An error to be injected into a tool call."""
33+
34+
injected_http_error_code: int
35+
"""Inject http error code to the tool call. Will present as "error_code"
36+
in the tool response dict."""
37+
38+
error_message: str
39+
"""Inject error message to the tool call. Will present as
40+
"error_message" in the tool response dict."""
41+
42+
43+
class InjectionConfig(BaseModel):
44+
"""Injection configuration for a tool."""
45+
46+
injection_probability: float = 1.0
47+
"""Probability of injecting the injected_value."""
48+
49+
match_args: Optional[Dict[str, Any]] = None
50+
"""Only apply injection if the request matches the match_args.
51+
If match_args is not provided, the injection will be applied to all
52+
requests."""
53+
54+
injected_latency_seconds: float = Field(default=0.0, le=120.0)
55+
"""Inject latency to the tool call. Please note it may not be accurate if │
56+
the interceptor is applied as after tool callback."""
57+
58+
random_seed: Optional[int] = None
59+
"""The random seed to use for this injection."""
60+
61+
injected_error: Optional[InjectedError] = None
62+
"""The injected error."""
63+
64+
injected_response: Optional[Dict[str, Any]] = None
65+
"""The injected response."""
66+
67+
@model_validator(mode="after")
68+
def check_injected_error_or_response(self) -> Self:
69+
"""Checks that either injected_error or injected_response is set."""
70+
if bool(self.injected_error) == bool(self.injected_response):
71+
raise ValueError(
72+
"Either injected_error or injected_response must be set, but not"
73+
" both, and not neither."
74+
)
75+
return self
76+
77+
78+
class MockStrategy(enum.Enum):
79+
"""Mock strategy for a tool."""
80+
81+
MOCK_STRATEGY_UNSPECIFIED = 0
82+
83+
MOCK_STRATEGY_TOOL_SPEC = 1
84+
"""Use tool specifications to mock the tool response."""
85+
86+
MOCK_STRATEGY_TRACING = 2
87+
"""Use provided tracing and tool specifications to mock the tool
88+
response based on llm response. Need to provide tracing path in
89+
command."""
90+
91+
92+
class ToolSimulationConfig(BaseModel):
93+
"""Simulation configuration for a single tool."""
94+
95+
tool_name: str
96+
"""Name of the tool to be simulated."""
97+
98+
injection_configs: List[InjectionConfig] = Field(default_factory=list)
99+
"""Injection configuration for the tool. If provided, the tool will be
100+
injected with the injected_value with the injection_probability first,
101+
the mock_strategy will be applied if no injection config is hit."""
102+
103+
mock_strategy_type: MockStrategy = MockStrategy.MOCK_STRATEGY_UNSPECIFIED
104+
"""The mock strategy to use."""
105+
106+
@model_validator(mode="after")
107+
def check_mock_strategy_type(self) -> Self:
108+
"""Checks that mock_strategy_type is not UNSPECIFIED if no injections."""
109+
if (
110+
not self.injection_configs
111+
and self.mock_strategy_type == MockStrategy.MOCK_STRATEGY_UNSPECIFIED
112+
):
113+
raise ValueError(
114+
"If injection_configs is empty, mock_strategy_type cannot be"
115+
" MOCK_STRATEGY_UNSPECIFIED."
116+
)
117+
return self
118+
119+
120+
class AgentSimulatorConfig(BaseModel):
121+
"""Configuration for AgentSimulator."""
122+
123+
tool_simulation_configs: List[ToolSimulationConfig] = Field(
124+
default_factory=list
125+
)
126+
"""A list of tool simulation configurations."""
127+
128+
simulation_model: str = Field(default="gemini-2.5-flash")
129+
"""The model to use for internal simulator LLM calls (tool analysis, mock responses)."""
130+
131+
simulation_model_configuration: genai_types.GenerateContentConfig = Field(
132+
default_factory=lambda: genai_types.GenerateContentConfig(
133+
thinking_config=genai_types.ThinkingConfig(
134+
include_thoughts=True,
135+
thinking_budget=10240,
136+
)
137+
),
138+
)
139+
"""The configuration for the internal simulator LLM calls."""
140+
141+
tracing_path: Optional[str] = None
142+
"""The path to the tracing file to be used for mocking. Only used if the
143+
mock_strategy_type is MOCK_STRATEGY_TRACING."""
144+
145+
@field_validator("tool_simulation_configs")
146+
@classmethod
147+
def check_tool_simulation_configs(cls, v: List[ToolSimulationConfig]):
148+
"""Checks that tool_simulation_configs is not empty."""
149+
if not v:
150+
raise ValueError("tool_simulation_configs must be provided.")
151+
seen_tool_names = set()
152+
for tool_sim_config in v:
153+
if tool_sim_config.tool_name in seen_tool_names:
154+
raise ValueError(
155+
f"Duplicate tool_name found: {tool_sim_config.tool_name}"
156+
)
157+
seen_tool_names.add(tool_sim_config.tool_name)
158+
return v
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Copyright 2026 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 asyncio
18+
import concurrent.futures
19+
import logging
20+
import random
21+
import time
22+
from typing import Any
23+
from typing import Dict
24+
from typing import Optional
25+
26+
agent_simulator_logger = logging.getLogger("agent_simulator_logger")
27+
28+
from google.adk.agents.llm_agent import LlmAgent
29+
from google.adk.tools.agent_simulator.agent_simulator_config import AgentSimulatorConfig
30+
from google.adk.tools.agent_simulator.agent_simulator_config import MockStrategy as MockStrategyEnum
31+
from google.adk.tools.agent_simulator.agent_simulator_config import ToolSimulationConfig
32+
from google.adk.tools.agent_simulator.strategies import base as base_mock_strategies
33+
from google.adk.tools.agent_simulator.strategies import tool_spec_mock_strategy
34+
from google.adk.tools.agent_simulator.tool_connection_analyzer import ToolConnectionAnalyzer
35+
from google.adk.tools.agent_simulator.tool_connection_map import ToolConnectionMap
36+
from google.adk.tools.base_tool import BaseTool
37+
38+
39+
def _create_mock_strategy(
40+
mock_strategy_type: MockStrategyEnum,
41+
llm_name: str,
42+
llm_config: genai_types.GenerateContentConfig,
43+
) -> base_mock_strategies.MockStrategy:
44+
"""Creates a mock strategy based on the given type."""
45+
if mock_strategy_type == MockStrategyEnum.MOCK_STRATEGY_TOOL_SPEC:
46+
return tool_spec_mock_strategy.ToolSpecMockStrategy(llm_name, llm_config)
47+
if mock_strategy_type == MockStrategyEnum.MOCK_STRATEGY_TRACING:
48+
return base_mock_strategies.TracingMockStrategy()
49+
raise ValueError(f"Unknown mock strategy type: {mock_strategy_type}")
50+
51+
52+
class AgentSimulatorEngine:
53+
"""Core engine to handle the simulation logic."""
54+
55+
def __init__(self, config: AgentSimulatorConfig):
56+
self._config = config
57+
self._tool_sim_configs = {
58+
c.tool_name: c for c in config.tool_simulation_configs
59+
}
60+
self._is_analyzed = False
61+
self._tool_connection_map: Optional[ToolConnectionMap] = None
62+
self._analyzer = ToolConnectionAnalyzer(
63+
llm_name=config.simulation_model,
64+
llm_config=config.simulation_model_configuration,
65+
)
66+
self._state_store = {}
67+
self._random_generator = random.Random()
68+
69+
async def simulate(
70+
self, tool: BaseTool, args: Dict[str, Any], tool_context: Any
71+
) -> Optional[Dict[str, Any]]:
72+
"""Simulates a tool call."""
73+
if tool.name not in self._tool_sim_configs:
74+
return None
75+
76+
tool_sim_config = self._tool_sim_configs[tool.name]
77+
78+
if not self._is_analyzed and any(
79+
c.mock_strategy_type != MockStrategyEnum.MOCK_STRATEGY_UNSPECIFIED
80+
for c in self._config.tool_simulation_configs
81+
):
82+
agent = tool_context._invocation_context.agent
83+
if isinstance(agent, LlmAgent):
84+
tools = await agent.canonical_tools(tool_context)
85+
self._tool_connection_map = await self._analyzer.analyze(tools)
86+
self._is_analyzed = True
87+
88+
for injection_config in tool_sim_config.injection_configs:
89+
if injection_config.match_args:
90+
if not all(
91+
item in args.items() for item in injection_config.match_args.items()
92+
):
93+
continue
94+
95+
if injection_config.random_seed is not None:
96+
self._random_generator.seed(injection_config.random_seed)
97+
98+
if (
99+
self._random_generator.random()
100+
< injection_config.injection_probability
101+
):
102+
time.sleep(injection_config.injected_latency_seconds)
103+
if injection_config.injected_error:
104+
return {
105+
"error_code": (
106+
injection_config.injected_error.injected_http_error_code
107+
),
108+
"error_message": injection_config.injected_error.error_message,
109+
}
110+
if injection_config.injected_response:
111+
return injection_config.injected_response
112+
113+
# If no injection was applied, fall back to the mock strategy.
114+
if (
115+
tool_sim_config.mock_strategy_type
116+
== MockStrategyEnum.MOCK_STRATEGY_UNSPECIFIED
117+
):
118+
agent_simulator_logger.warning(
119+
"Tool '%s' did not hit any injection config and has no mock strategy"
120+
" configured. Returning no-op.",
121+
tool.name,
122+
)
123+
return None
124+
125+
mock_strategy = _create_mock_strategy(
126+
tool_sim_config.mock_strategy_type,
127+
self._config.simulation_model,
128+
self._config.simulation_model_configuration,
129+
)
130+
return await mock_strategy.mock(
131+
tool, args, tool_context, self._tool_connection_map, self._state_store
132+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright 2026 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+
from typing import Any
18+
from typing import Awaitable
19+
from typing import Callable
20+
from typing import Dict
21+
from typing import Optional
22+
23+
from google.adk.tools.agent_simulator.agent_simulator_config import AgentSimulatorConfig
24+
from google.adk.tools.agent_simulator.agent_simulator_engine import AgentSimulatorEngine
25+
from google.adk.tools.agent_simulator.agent_simulator_plugin import AgentSimulatorPlugin
26+
from google.adk.tools.base_tool import BaseTool
27+
28+
from ...utils.feature_decorator import experimental
29+
30+
31+
@experimental
32+
class AgentSimulatorFactory:
33+
"""Factory for creating AgentSimulator instances."""
34+
35+
@staticmethod
36+
def create_callback(
37+
config: AgentSimulatorConfig,
38+
) -> Callable[
39+
[BaseTool, Dict[str, Any], Any], Awaitable[Optional[Dict[str, Any]]]
40+
]:
41+
"""Creates a callback function for AgentSimulator.
42+
43+
Args:
44+
config: The configuration for the AgentSimulator.
45+
46+
Returns:
47+
A callable that can be used as a before_tool_callback or after_tool_callback.
48+
"""
49+
simulator_engine = AgentSimulatorEngine(config)
50+
51+
async def _agent_simulator_callback(
52+
tool: BaseTool, args: Dict[str, Any], tool_context: Any
53+
) -> Optional[Dict[str, Any]]:
54+
return await simulator_engine.simulate(tool, args, tool_context)
55+
56+
return _agent_simulator_callback
57+
58+
@staticmethod
59+
def create_plugin(
60+
config: AgentSimulatorConfig,
61+
) -> AgentSimulatorPlugin:
62+
"""Creates an ADK Plugin for AgentSimulator.
63+
64+
Args:
65+
config: The configuration for the AgentSimulator.
66+
67+
Returns:
68+
An instance of AgentSimulatorPlugin that can be used as an ADK plugin.
69+
"""
70+
simulator_engine = AgentSimulatorEngine(config)
71+
return AgentSimulatorPlugin(simulator_engine)

0 commit comments

Comments
 (0)