You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93a085ad87 | |||
| 6645aa07fd | |||
| 3686a3a98f | |||
| ae993e884f | |||
| bb89466623 | |||
| adbc37fea1 | |||
| 9b112e2d13 | |||
| a08bf62b95 | |||
| e752bbb756 |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [1.24.1](https://github.com/google/adk-python/compare/v1.24.0...v1.24.1) (2026-02-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add back deprecated eval endpoint for web until we migrate([ae993e8](https://github.com/google/adk-python/commit/ae993e884f44db276a4116ebb7a11a2fb586dbfe))
|
||||
* Update eval dialog colors, and fix a2ui component types ([3686a3a](https://github.com/google/adk-python/commit/3686a3a98f46738549cd7a999f3773b7a6fd1182))
|
||||
|
||||
## [1.24.0](https://github.com/google/adk-python/compare/v1.23.0...v1.24.0) (2026-02-04)
|
||||
|
||||
### âš BREAKING CHANGES
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 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 . import agent
|
||||
@@ -0,0 +1,166 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
"""Sample agent demonstrating MCP progress callback feature.
|
||||
|
||||
This sample shows how to use the progress_callback parameter in McpToolset
|
||||
to receive progress notifications from MCP servers during long-running tool
|
||||
executions.
|
||||
|
||||
There are two ways to use progress callbacks:
|
||||
|
||||
1. Simple callback (shared by all tools):
|
||||
Pass a ProgressFnT callback that receives (progress, total, message).
|
||||
|
||||
2. Factory function (per-tool callbacks with runtime context):
|
||||
Pass a ProgressCallbackFactory that takes (tool_name, callback_context, **kwargs)
|
||||
and returns a ProgressFnT or None. This allows different tools to have different
|
||||
progress handling logic, and the factory can access and modify session state
|
||||
via the CallbackContext. The **kwargs ensures forward compatibility for future
|
||||
parameters.
|
||||
|
||||
IMPORTANT: Progress callbacks only work when the MCP server actually sends
|
||||
progress notifications. Most simple MCP servers (like the filesystem server)
|
||||
do not send progress updates. This sample uses a mock server that demonstrates
|
||||
progress reporting.
|
||||
|
||||
Usage:
|
||||
adk run contributing/samples/mcp_progress_callback_agent
|
||||
|
||||
Then try:
|
||||
"Run the long running task with 5 steps"
|
||||
"Process these items: apple, banana, cherry"
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.tools.mcp_tool import McpToolset
|
||||
from google.adk.tools.mcp_tool import StdioConnectionParams
|
||||
from mcp import StdioServerParameters
|
||||
from mcp.shared.session import ProgressFnT
|
||||
|
||||
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
_mock_server_path = os.path.join(_current_dir, "mock_progress_server.py")
|
||||
|
||||
|
||||
# Option 1: Simple shared callback
|
||||
async def simple_progress_callback(
|
||||
progress: float,
|
||||
total: float | None,
|
||||
message: str | None,
|
||||
) -> None:
|
||||
"""Handle progress notifications from MCP server.
|
||||
|
||||
This callback is shared by all tools in the toolset.
|
||||
"""
|
||||
if total is not None:
|
||||
percentage = (progress / total) * 100
|
||||
bar_length = 20
|
||||
filled = int(bar_length * progress / total)
|
||||
bar = "=" * filled + "-" * (bar_length - filled)
|
||||
print(f"[{bar}] {percentage:.0f}% ({progress}/{total}) {message or ''}")
|
||||
else:
|
||||
print(f"Progress: {progress} {f'- {message}' if message else ''}")
|
||||
|
||||
|
||||
# Option 2: Factory function for per-tool callbacks with runtime context
|
||||
def progress_callback_factory(
|
||||
tool_name: str,
|
||||
*,
|
||||
callback_context: CallbackContext | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ProgressFnT | None:
|
||||
"""Create a progress callback for a specific tool.
|
||||
|
||||
This factory allows different tools to have different progress handling.
|
||||
It receives a CallbackContext for accessing and modifying runtime information
|
||||
like session state. The **kwargs parameter ensures forward compatibility.
|
||||
|
||||
Args:
|
||||
tool_name: The name of the MCP tool.
|
||||
callback_context: The callback context providing access to session,
|
||||
state, artifacts, and other runtime information. Allows modifying
|
||||
state via ctx.state['key'] = value. May be None if not available.
|
||||
**kwargs: Additional keyword arguments for future extensibility.
|
||||
|
||||
Returns:
|
||||
A progress callback function, or None if no callback is needed.
|
||||
"""
|
||||
# Example: Access session info from context (if available)
|
||||
session_id = "unknown"
|
||||
if callback_context and callback_context.session:
|
||||
session_id = callback_context.session.id
|
||||
|
||||
async def callback(
|
||||
progress: float,
|
||||
total: float | None,
|
||||
message: str | None,
|
||||
) -> None:
|
||||
# Include tool name and session info in the progress output
|
||||
prefix = f"[{tool_name}][session:{session_id}]"
|
||||
if total is not None:
|
||||
percentage = (progress / total) * 100
|
||||
bar_length = 20
|
||||
filled = int(bar_length * progress / total)
|
||||
bar = "=" * filled + "-" * (bar_length - filled)
|
||||
print(f"{prefix} [{bar}] {percentage:.0f}% {message or ''}")
|
||||
# Example: Store progress in state (callback_context allows modification)
|
||||
if callback_context:
|
||||
callback_context.state["last_progress"] = progress
|
||||
callback_context.state["last_total"] = total
|
||||
else:
|
||||
print(
|
||||
f"{prefix} Progress: {progress} {f'- {message}' if message else ''}"
|
||||
)
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
root_agent = LlmAgent(
|
||||
model="gemini-2.5-flash",
|
||||
name="progress_demo_agent",
|
||||
instruction="""\
|
||||
You are a helpful assistant that can run long-running tasks.
|
||||
|
||||
Available tools:
|
||||
- long_running_task: Simulates a task with multiple steps. You can specify
|
||||
the number of steps and delay between them.
|
||||
- process_items: Processes a list of items one by one with progress updates.
|
||||
|
||||
When the user asks you to run a task, use these tools and the progress
|
||||
will be logged automatically.
|
||||
|
||||
Example requests:
|
||||
- "Run a long task with 5 steps"
|
||||
- "Process these items: apple, banana, cherry, date"
|
||||
""",
|
||||
tools=[
|
||||
McpToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command=sys.executable, # Use current Python interpreter
|
||||
args=[_mock_server_path],
|
||||
),
|
||||
timeout=60,
|
||||
),
|
||||
# Use factory function for per-tool callbacks (Option 2)
|
||||
# Or use simple_progress_callback for shared callback (Option 1)
|
||||
progress_callback=progress_callback_factory,
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
"""Mock MCP server that sends progress notifications.
|
||||
|
||||
This server demonstrates how MCP servers can send progress updates
|
||||
during long-running tool execution.
|
||||
|
||||
Run this server directly:
|
||||
python mock_progress_server.py
|
||||
|
||||
Or use it with the sample agent:
|
||||
See agent_with_mock_server.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import TextContent
|
||||
from mcp.types import Tool
|
||||
|
||||
server = Server("mock-progress-server")
|
||||
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[Tool]:
|
||||
"""List available tools."""
|
||||
return [
|
||||
Tool(
|
||||
name="long_running_task",
|
||||
description=(
|
||||
"A simulated long-running task that reports progress. "
|
||||
"Use this to test progress callback functionality."
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"steps": {
|
||||
"type": "integer",
|
||||
"description": "Number of steps to simulate (default: 5)",
|
||||
"default": 5,
|
||||
},
|
||||
"delay": {
|
||||
"type": "number",
|
||||
"description": (
|
||||
"Delay in seconds between steps (default: 0.5)"
|
||||
),
|
||||
"default": 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
Tool(
|
||||
name="process_items",
|
||||
description="Process a list of items with progress reporting.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of items to process",
|
||||
},
|
||||
},
|
||||
"required": ["items"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||
"""Handle tool calls with progress reporting."""
|
||||
ctx = server.request_context
|
||||
|
||||
if name == "long_running_task":
|
||||
steps = arguments.get("steps", 5)
|
||||
delay = arguments.get("delay", 0.5)
|
||||
|
||||
# Get progress token from request metadata
|
||||
progress_token = None
|
||||
if ctx.meta and hasattr(ctx.meta, "progressToken"):
|
||||
progress_token = ctx.meta.progressToken
|
||||
|
||||
for i in range(steps):
|
||||
# Simulate work
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# Send progress notification if client supports it
|
||||
if progress_token is not None:
|
||||
await ctx.session.send_progress_notification(
|
||||
progress_token=progress_token,
|
||||
progress=i + 1,
|
||||
total=steps,
|
||||
message=f"Completed step {i + 1} of {steps}",
|
||||
)
|
||||
|
||||
return [
|
||||
TextContent(
|
||||
type="text",
|
||||
text=f"Successfully completed {steps} steps!",
|
||||
)
|
||||
]
|
||||
|
||||
elif name == "process_items":
|
||||
items = arguments.get("items", [])
|
||||
total = len(items)
|
||||
|
||||
progress_token = None
|
||||
if ctx.meta and hasattr(ctx.meta, "progressToken"):
|
||||
progress_token = ctx.meta.progressToken
|
||||
|
||||
results = []
|
||||
for i, item in enumerate(items):
|
||||
# Simulate processing
|
||||
await asyncio.sleep(0.3)
|
||||
results.append(f"Processed: {item}")
|
||||
|
||||
# Send progress
|
||||
if progress_token is not None:
|
||||
await ctx.session.send_progress_notification(
|
||||
progress_token=progress_token,
|
||||
progress=i + 1,
|
||||
total=total,
|
||||
message=f"Processing item: {item}",
|
||||
)
|
||||
|
||||
return [
|
||||
TextContent(
|
||||
type="text",
|
||||
text="\n".join(results),
|
||||
)
|
||||
]
|
||||
|
||||
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the MCP server."""
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
server.create_initialization_options(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -219,7 +219,6 @@ asyncio_mode = "auto"
|
||||
python_version = "3.10"
|
||||
exclude = ["tests/", "contributing/samples/"]
|
||||
plugins = ["pydantic.mypy"]
|
||||
# Start with non-strict mode, and swtich to strict mode later.
|
||||
strict = true
|
||||
disable_error_code = ["import-not-found", "import-untyped", "unused-ignore"]
|
||||
follow_imports = "skip"
|
||||
|
||||
@@ -59,7 +59,9 @@ def _to_a2a_context_id(app_name: str, user_id: str, session_id: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _from_a2a_context_id(context_id: str) -> tuple[str, str, str]:
|
||||
def _from_a2a_context_id(
|
||||
context_id: str | None,
|
||||
) -> tuple[str, str, str] | tuple[None, None, None]:
|
||||
"""Converts an A2A context id to app name, user id and session id.
|
||||
if context_id is None, return None, None, None
|
||||
if context_id is not None, but not in the format of
|
||||
@@ -69,7 +71,7 @@ def _from_a2a_context_id(context_id: str) -> tuple[str, str, str]:
|
||||
context_id: The A2A context id.
|
||||
|
||||
Returns:
|
||||
The app name, user id and session id.
|
||||
The app name, user id and session id, or (None, None, None) if invalid.
|
||||
"""
|
||||
if not context_id:
|
||||
return None, None, None
|
||||
|
||||
@@ -946,6 +946,96 @@ class AdkWebServer:
|
||||
detail=str(ve),
|
||||
) from ve
|
||||
|
||||
# TODO - remove after migration
|
||||
@deprecated(
|
||||
"Please use create_eval_set instead. This will be removed in future"
|
||||
" releases."
|
||||
)
|
||||
@app.post(
|
||||
"/apps/{app_name}/eval_sets/{eval_set_id}",
|
||||
response_model_exclude_none=True,
|
||||
tags=[TAG_EVALUATION],
|
||||
)
|
||||
async def create_eval_set_legacy(
|
||||
app_name: str,
|
||||
eval_set_id: str,
|
||||
):
|
||||
"""Creates an eval set, given the id."""
|
||||
await create_eval_set(
|
||||
app_name=app_name,
|
||||
create_eval_set_request=CreateEvalSetRequest(
|
||||
eval_set=EvalSet(eval_set_id=eval_set_id, eval_cases=[])
|
||||
),
|
||||
)
|
||||
|
||||
# TODO - remove after migration
|
||||
@deprecated(
|
||||
"Please use list_eval_sets instead. This will be removed in future"
|
||||
" releases."
|
||||
)
|
||||
@app.get(
|
||||
"/apps/{app_name}/eval_sets",
|
||||
response_model_exclude_none=True,
|
||||
tags=[TAG_EVALUATION],
|
||||
)
|
||||
async def list_eval_sets_legacy(app_name: str) -> list[str]:
|
||||
list_eval_sets_response = await list_eval_sets(app_name)
|
||||
return list_eval_sets_response.eval_set_ids
|
||||
|
||||
# TODO - remove after migration
|
||||
@deprecated(
|
||||
"Please use run_eval instead. This will be removed in future releases."
|
||||
)
|
||||
@app.post(
|
||||
"/apps/{app_name}/eval_sets/{eval_set_id}/run_eval",
|
||||
response_model_exclude_none=True,
|
||||
tags=[TAG_EVALUATION],
|
||||
)
|
||||
async def run_eval_legacy(
|
||||
app_name: str, eval_set_id: str, req: RunEvalRequest
|
||||
) -> list[RunEvalResult]:
|
||||
run_eval_response = await run_eval(
|
||||
app_name=app_name, eval_set_id=eval_set_id, req=req
|
||||
)
|
||||
return run_eval_response.run_eval_results
|
||||
|
||||
# TODO - remove after migration
|
||||
@deprecated(
|
||||
"Please use get_eval_result instead. This will be removed in future"
|
||||
" releases."
|
||||
)
|
||||
@app.get(
|
||||
"/apps/{app_name}/eval_results/{eval_result_id}",
|
||||
response_model_exclude_none=True,
|
||||
tags=[TAG_EVALUATION],
|
||||
)
|
||||
async def get_eval_result_legacy(
|
||||
app_name: str,
|
||||
eval_result_id: str,
|
||||
) -> EvalSetResult:
|
||||
try:
|
||||
return self.eval_set_results_manager.get_eval_set_result(
|
||||
app_name, eval_result_id
|
||||
)
|
||||
except ValueError as ve:
|
||||
raise HTTPException(status_code=404, detail=str(ve)) from ve
|
||||
except ValidationError as ve:
|
||||
raise HTTPException(status_code=500, detail=str(ve)) from ve
|
||||
|
||||
# TODO - remove after migration
|
||||
@deprecated(
|
||||
"Please use list_eval_results instead. This will be removed in future"
|
||||
" releases."
|
||||
)
|
||||
@app.get(
|
||||
"/apps/{app_name}/eval_results",
|
||||
response_model_exclude_none=True,
|
||||
tags=[TAG_EVALUATION],
|
||||
)
|
||||
async def list_eval_results_legacy(app_name: str) -> list[str]:
|
||||
list_eval_results_response = await list_eval_results(app_name)
|
||||
return list_eval_results_response.eval_result_ids
|
||||
|
||||
@app.get(
|
||||
"/apps/{app_name}/eval-sets",
|
||||
response_model_exclude_none=True,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Copyright 2025 Google LLC
|
||||
Copyright 2026 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -30,5 +30,5 @@
|
||||
<style>html{--mat-sys-corner-extra-large:28px;--mat-sys-corner-extra-large-top:28px 28px 0 0;--mat-sys-corner-extra-small:4px;--mat-sys-corner-extra-small-top:4px 4px 0 0;--mat-sys-corner-full:9999px;--mat-sys-corner-large:16px;--mat-sys-corner-large-end:0 16px 16px 0;--mat-sys-corner-large-start:16px 0 0 16px;--mat-sys-corner-large-top:16px 16px 0 0;--mat-sys-corner-medium:12px;--mat-sys-corner-none:0;--mat-sys-corner-small:8px;--mat-sys-dragged-state-layer-opacity:.16;--mat-sys-focus-state-layer-opacity:.12;--mat-sys-hover-state-layer-opacity:.08;--mat-sys-pressed-state-layer-opacity:.12}html{font-family:Google Sans,Helvetica Neue,sans-serif!important}body{height:100vh;margin:0}</style><link rel="stylesheet" href="./styles-SI5RXIFC.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="./styles-SI5RXIFC.css"></noscript></head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<link rel="modulepreload" href="./chunk-BX7YU7E6.js"><link rel="modulepreload" href="./chunk-W7GRJBO5.js"><script src="./polyfills-5CFQRCPP.js" type="module"></script><script src="./main-AB4G4EO6.js" type="module"></script></body>
|
||||
<link rel="modulepreload" href="./chunk-BX7YU7E6.js"><link rel="modulepreload" href="./chunk-W7GRJBO5.js"><script src="./polyfills-5CFQRCPP.js" type="module"></script><script src="./main-QQBY56NS.js" type="module"></script></body>
|
||||
</html>
|
||||
|
||||
+108
-108
File diff suppressed because one or more lines are too long
@@ -113,6 +113,12 @@ class CacheMetadata(BaseModel):
|
||||
f"fingerprint={self.fingerprint[:8]}..."
|
||||
)
|
||||
cache_id = self.cache_name.split("/")[-1]
|
||||
if self.expire_time is None:
|
||||
return (
|
||||
f"Cache {cache_id}: used {self.invocations_used} invocations, "
|
||||
f"cached {self.contents_count} contents, "
|
||||
"expires unknown"
|
||||
)
|
||||
time_until_expiry_minutes = (self.expire_time - time.time()) / 60
|
||||
return (
|
||||
f"Cache {cache_id}: used {self.invocations_used} invocations, "
|
||||
|
||||
@@ -17,13 +17,11 @@ from __future__ import annotations
|
||||
from collections.abc import Sequence
|
||||
import logging
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from google.genai import types
|
||||
|
||||
from ..agents.callback_context import CallbackContext
|
||||
from ..events.event import Event
|
||||
from ..models.llm_request import LlmRequest
|
||||
from ..models.llm_response import LlmResponse
|
||||
from .base_plugin import BasePlugin
|
||||
@@ -62,21 +60,61 @@ def _adjust_split_index_to_avoid_orphaned_function_responses(
|
||||
return 0
|
||||
|
||||
|
||||
def _is_function_response_content(content: types.Content) -> bool:
|
||||
"""Returns whether a content contains function responses."""
|
||||
return bool(content.parts) and any(
|
||||
part.function_response is not None for part in content.parts
|
||||
)
|
||||
|
||||
|
||||
def _is_human_user_content(content: types.Content) -> bool:
|
||||
"""Returns whether a content represents user input (not tool output)."""
|
||||
return content.role == "user" and not _is_function_response_content(content)
|
||||
|
||||
|
||||
def _get_invocation_start_indices(
|
||||
contents: Sequence[types.Content],
|
||||
) -> list[int]:
|
||||
"""Returns indices that begin a user-started invocation.
|
||||
|
||||
An invocation begins with one or more consecutive user messages. Tool outputs
|
||||
(function responses) are role="user" but are *not* considered invocation
|
||||
starts.
|
||||
|
||||
Args:
|
||||
contents: Full conversation contents in chronological order.
|
||||
|
||||
Returns:
|
||||
A list of indices where each index marks the beginning of an invocation.
|
||||
"""
|
||||
invocation_start_indices = []
|
||||
previous_was_human_user = False
|
||||
for i, content in enumerate(contents):
|
||||
is_human_user = _is_human_user_content(content)
|
||||
if is_human_user and not previous_was_human_user:
|
||||
invocation_start_indices.append(i)
|
||||
previous_was_human_user = is_human_user
|
||||
return invocation_start_indices
|
||||
|
||||
|
||||
class ContextFilterPlugin(BasePlugin):
|
||||
"""A plugin that filters the LLM context to reduce its size."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_invocations_to_keep: Optional[int] = None,
|
||||
custom_filter: Optional[Callable[[List[Event]], List[Event]]] = None,
|
||||
custom_filter: Optional[
|
||||
Callable[[list[types.Content]], list[types.Content]]
|
||||
] = None,
|
||||
name: str = "context_filter_plugin",
|
||||
):
|
||||
"""Initializes the context management plugin.
|
||||
|
||||
Args:
|
||||
num_invocations_to_keep: The number of last invocations to keep. An
|
||||
invocation is defined as one or more consecutive user messages followed
|
||||
by a model response.
|
||||
invocation starts with one or more consecutive user messages and can
|
||||
contain multiple model turns (e.g. tool calls) until the next user
|
||||
message starts a new invocation.
|
||||
custom_filter: A function to filter the context.
|
||||
name: The name of the plugin instance.
|
||||
"""
|
||||
@@ -89,27 +127,16 @@ class ContextFilterPlugin(BasePlugin):
|
||||
) -> Optional[LlmResponse]:
|
||||
"""Filters the LLM request's context before it is sent to the model."""
|
||||
try:
|
||||
contents = llm_request.contents
|
||||
contents: list[types.Content] = llm_request.contents
|
||||
|
||||
if (
|
||||
self._num_invocations_to_keep is not None
|
||||
and self._num_invocations_to_keep > 0
|
||||
):
|
||||
num_model_turns = sum(1 for c in contents if c.role == "model")
|
||||
if num_model_turns >= self._num_invocations_to_keep:
|
||||
model_turns_to_find = self._num_invocations_to_keep
|
||||
split_index = 0
|
||||
for i in range(len(contents) - 1, -1, -1):
|
||||
if contents[i].role == "model":
|
||||
model_turns_to_find -= 1
|
||||
if model_turns_to_find == 0:
|
||||
start_index = i
|
||||
while (
|
||||
start_index > 0 and contents[start_index - 1].role == "user"
|
||||
):
|
||||
start_index -= 1
|
||||
split_index = start_index
|
||||
break
|
||||
invocation_start_indices = _get_invocation_start_indices(contents)
|
||||
if len(invocation_start_indices) > self._num_invocations_to_keep:
|
||||
split_index = invocation_start_indices[-self._num_invocations_to_keep]
|
||||
|
||||
# Adjust split_index to avoid orphaned function_responses.
|
||||
split_index = (
|
||||
_adjust_split_index_to_avoid_orphaned_function_responses(
|
||||
@@ -122,7 +149,7 @@ class ContextFilterPlugin(BasePlugin):
|
||||
contents = self._custom_filter(contents)
|
||||
|
||||
llm_request.contents = contents
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reduce context for request: {e}")
|
||||
except Exception:
|
||||
logger.exception("Failed to reduce context for request")
|
||||
|
||||
return None
|
||||
|
||||
@@ -35,7 +35,6 @@ from sqlalchemy.ext.asyncio import AsyncSession as DatabaseSessionFactory
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from typing_extensions import override
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from . import _session_util
|
||||
from ..errors.already_exists_error import AlreadyExistsError
|
||||
@@ -134,10 +133,6 @@ class DatabaseSessionService(BaseSessionService):
|
||||
f"Failed to create database engine for URL '{db_url}'"
|
||||
) from e
|
||||
|
||||
# Get the local timezone
|
||||
local_timezone = get_localzone()
|
||||
logger.info("Local timezone: %s", local_timezone)
|
||||
|
||||
self.db_engine: AsyncEngine = db_engine
|
||||
|
||||
# DB session factory method
|
||||
|
||||
@@ -52,6 +52,7 @@ from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_A
|
||||
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_USAGE_INPUT_TOKENS
|
||||
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_USAGE_OUTPUT_TOKENS
|
||||
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GenAiSystemValues
|
||||
from opentelemetry.semconv._incubating.attributes.user_attributes import USER_ID
|
||||
from opentelemetry.semconv.schemas import Schemas
|
||||
from opentelemetry.trace import Span
|
||||
from opentelemetry.util.types import AnyValue
|
||||
@@ -438,8 +439,11 @@ def use_generate_content_span(
|
||||
"""
|
||||
|
||||
common_attributes = {
|
||||
GEN_AI_AGENT_NAME: invocation_context.agent.name,
|
||||
GEN_AI_CONVERSATION_ID: invocation_context.session.id,
|
||||
USER_ID: invocation_context.session.user_id,
|
||||
'gcp.vertex.agent.event_id': model_response_event.id,
|
||||
'gcp.vertex.agent.invocation_id': invocation_context.invocation_id,
|
||||
}
|
||||
if (
|
||||
_is_gemini_agent(invocation_context.agent)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Copyright 2026 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 google.adk.tools.agent_simulator.agent_simulator_factory import AgentSimulatorFactory
|
||||
|
||||
__all__ = ["AgentSimulator"]
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright 2026 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
|
||||
|
||||
import enum
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from google.genai import types as genai_types
|
||||
from pydantic import BaseModel
|
||||
from pydantic import Field
|
||||
from pydantic import field_validator
|
||||
from pydantic import model_validator
|
||||
from pydantic_core import ValidationError
|
||||
|
||||
|
||||
class InjectedError(BaseModel):
|
||||
"""An error to be injected into a tool call."""
|
||||
|
||||
injected_http_error_code: int
|
||||
"""Inject http error code to the tool call. Will present as "error_code"
|
||||
in the tool response dict."""
|
||||
|
||||
error_message: str
|
||||
"""Inject error message to the tool call. Will present as
|
||||
"error_message" in the tool response dict."""
|
||||
|
||||
|
||||
class InjectionConfig(BaseModel):
|
||||
"""Injection configuration for a tool."""
|
||||
|
||||
injection_probability: float = 1.0
|
||||
"""Probability of injecting the injected_value."""
|
||||
|
||||
match_args: Optional[Dict[str, Any]] = None
|
||||
"""Only apply injection if the request matches the match_args.
|
||||
If match_args is not provided, the injection will be applied to all
|
||||
requests."""
|
||||
|
||||
injected_latency_seconds: float = Field(default=0.0, le=120.0)
|
||||
"""Inject latency to the tool call. Please note it may not be accurate if │
|
||||
the interceptor is applied as after tool callback."""
|
||||
|
||||
random_seed: Optional[int] = None
|
||||
"""The random seed to use for this injection."""
|
||||
|
||||
injected_error: Optional[InjectedError] = None
|
||||
"""The injected error."""
|
||||
|
||||
injected_response: Optional[Dict[str, Any]] = None
|
||||
"""The injected response."""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_injected_error_or_response(self) -> Self:
|
||||
"""Checks that either injected_error or injected_response is set."""
|
||||
if bool(self.injected_error) == bool(self.injected_response):
|
||||
raise ValueError(
|
||||
"Either injected_error or injected_response must be set, but not"
|
||||
" both, and not neither."
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class MockStrategy(enum.Enum):
|
||||
"""Mock strategy for a tool."""
|
||||
|
||||
MOCK_STRATEGY_UNSPECIFIED = 0
|
||||
|
||||
MOCK_STRATEGY_TOOL_SPEC = 1
|
||||
"""Use tool specifications to mock the tool response."""
|
||||
|
||||
MOCK_STRATEGY_TRACING = 2
|
||||
"""Use provided tracing and tool specifications to mock the tool
|
||||
response based on llm response. Need to provide tracing path in
|
||||
command."""
|
||||
|
||||
|
||||
class ToolSimulationConfig(BaseModel):
|
||||
"""Simulation configuration for a single tool."""
|
||||
|
||||
tool_name: str
|
||||
"""Name of the tool to be simulated."""
|
||||
|
||||
injection_configs: List[InjectionConfig] = Field(default_factory=list)
|
||||
"""Injection configuration for the tool. If provided, the tool will be
|
||||
injected with the injected_value with the injection_probability first,
|
||||
the mock_strategy will be applied if no injection config is hit."""
|
||||
|
||||
mock_strategy_type: MockStrategy = MockStrategy.MOCK_STRATEGY_UNSPECIFIED
|
||||
"""The mock strategy to use."""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_mock_strategy_type(self) -> Self:
|
||||
"""Checks that mock_strategy_type is not UNSPECIFIED if no injections."""
|
||||
if (
|
||||
not self.injection_configs
|
||||
and self.mock_strategy_type == MockStrategy.MOCK_STRATEGY_UNSPECIFIED
|
||||
):
|
||||
raise ValueError(
|
||||
"If injection_configs is empty, mock_strategy_type cannot be"
|
||||
" MOCK_STRATEGY_UNSPECIFIED."
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class AgentSimulatorConfig(BaseModel):
|
||||
"""Configuration for AgentSimulator."""
|
||||
|
||||
tool_simulation_configs: List[ToolSimulationConfig] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
"""A list of tool simulation configurations."""
|
||||
|
||||
simulation_model: str = Field(default="gemini-2.5-flash")
|
||||
"""The model to use for internal simulator LLM calls (tool analysis, mock responses)."""
|
||||
|
||||
simulation_model_configuration: genai_types.GenerateContentConfig = Field(
|
||||
default_factory=lambda: genai_types.GenerateContentConfig(
|
||||
thinking_config=genai_types.ThinkingConfig(
|
||||
include_thoughts=True,
|
||||
thinking_budget=10240,
|
||||
)
|
||||
),
|
||||
)
|
||||
"""The configuration for the internal simulator LLM calls."""
|
||||
|
||||
tracing_path: Optional[str] = None
|
||||
"""The path to the tracing file to be used for mocking. Only used if the
|
||||
mock_strategy_type is MOCK_STRATEGY_TRACING."""
|
||||
|
||||
@field_validator("tool_simulation_configs")
|
||||
@classmethod
|
||||
def check_tool_simulation_configs(cls, v: List[ToolSimulationConfig]):
|
||||
"""Checks that tool_simulation_configs is not empty."""
|
||||
if not v:
|
||||
raise ValueError("tool_simulation_configs must be provided.")
|
||||
seen_tool_names = set()
|
||||
for tool_sim_config in v:
|
||||
if tool_sim_config.tool_name in seen_tool_names:
|
||||
raise ValueError(
|
||||
f"Duplicate tool_name found: {tool_sim_config.tool_name}"
|
||||
)
|
||||
seen_tool_names.add(tool_sim_config.tool_name)
|
||||
return v
|
||||
@@ -0,0 +1,132 @@
|
||||
# Copyright 2026 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
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
agent_simulator_logger = logging.getLogger("agent_simulator_logger")
|
||||
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.tools.agent_simulator.agent_simulator_config import AgentSimulatorConfig
|
||||
from google.adk.tools.agent_simulator.agent_simulator_config import MockStrategy as MockStrategyEnum
|
||||
from google.adk.tools.agent_simulator.agent_simulator_config import ToolSimulationConfig
|
||||
from google.adk.tools.agent_simulator.strategies import base as base_mock_strategies
|
||||
from google.adk.tools.agent_simulator.strategies import tool_spec_mock_strategy
|
||||
from google.adk.tools.agent_simulator.tool_connection_analyzer import ToolConnectionAnalyzer
|
||||
from google.adk.tools.agent_simulator.tool_connection_map import ToolConnectionMap
|
||||
from google.adk.tools.base_tool import BaseTool
|
||||
|
||||
|
||||
def _create_mock_strategy(
|
||||
mock_strategy_type: MockStrategyEnum,
|
||||
llm_name: str,
|
||||
llm_config: genai_types.GenerateContentConfig,
|
||||
) -> base_mock_strategies.MockStrategy:
|
||||
"""Creates a mock strategy based on the given type."""
|
||||
if mock_strategy_type == MockStrategyEnum.MOCK_STRATEGY_TOOL_SPEC:
|
||||
return tool_spec_mock_strategy.ToolSpecMockStrategy(llm_name, llm_config)
|
||||
if mock_strategy_type == MockStrategyEnum.MOCK_STRATEGY_TRACING:
|
||||
return base_mock_strategies.TracingMockStrategy()
|
||||
raise ValueError(f"Unknown mock strategy type: {mock_strategy_type}")
|
||||
|
||||
|
||||
class AgentSimulatorEngine:
|
||||
"""Core engine to handle the simulation logic."""
|
||||
|
||||
def __init__(self, config: AgentSimulatorConfig):
|
||||
self._config = config
|
||||
self._tool_sim_configs = {
|
||||
c.tool_name: c for c in config.tool_simulation_configs
|
||||
}
|
||||
self._is_analyzed = False
|
||||
self._tool_connection_map: Optional[ToolConnectionMap] = None
|
||||
self._analyzer = ToolConnectionAnalyzer(
|
||||
llm_name=config.simulation_model,
|
||||
llm_config=config.simulation_model_configuration,
|
||||
)
|
||||
self._state_store = {}
|
||||
self._random_generator = random.Random()
|
||||
|
||||
async def simulate(
|
||||
self, tool: BaseTool, args: Dict[str, Any], tool_context: Any
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Simulates a tool call."""
|
||||
if tool.name not in self._tool_sim_configs:
|
||||
return None
|
||||
|
||||
tool_sim_config = self._tool_sim_configs[tool.name]
|
||||
|
||||
if not self._is_analyzed and any(
|
||||
c.mock_strategy_type != MockStrategyEnum.MOCK_STRATEGY_UNSPECIFIED
|
||||
for c in self._config.tool_simulation_configs
|
||||
):
|
||||
agent = tool_context._invocation_context.agent
|
||||
if isinstance(agent, LlmAgent):
|
||||
tools = await agent.canonical_tools(tool_context)
|
||||
self._tool_connection_map = await self._analyzer.analyze(tools)
|
||||
self._is_analyzed = True
|
||||
|
||||
for injection_config in tool_sim_config.injection_configs:
|
||||
if injection_config.match_args:
|
||||
if not all(
|
||||
item in args.items() for item in injection_config.match_args.items()
|
||||
):
|
||||
continue
|
||||
|
||||
if injection_config.random_seed is not None:
|
||||
self._random_generator.seed(injection_config.random_seed)
|
||||
|
||||
if (
|
||||
self._random_generator.random()
|
||||
< injection_config.injection_probability
|
||||
):
|
||||
time.sleep(injection_config.injected_latency_seconds)
|
||||
if injection_config.injected_error:
|
||||
return {
|
||||
"error_code": (
|
||||
injection_config.injected_error.injected_http_error_code
|
||||
),
|
||||
"error_message": injection_config.injected_error.error_message,
|
||||
}
|
||||
if injection_config.injected_response:
|
||||
return injection_config.injected_response
|
||||
|
||||
# If no injection was applied, fall back to the mock strategy.
|
||||
if (
|
||||
tool_sim_config.mock_strategy_type
|
||||
== MockStrategyEnum.MOCK_STRATEGY_UNSPECIFIED
|
||||
):
|
||||
agent_simulator_logger.warning(
|
||||
"Tool '%s' did not hit any injection config and has no mock strategy"
|
||||
" configured. Returning no-op.",
|
||||
tool.name,
|
||||
)
|
||||
return None
|
||||
|
||||
mock_strategy = _create_mock_strategy(
|
||||
tool_sim_config.mock_strategy_type,
|
||||
self._config.simulation_model,
|
||||
self._config.simulation_model_configuration,
|
||||
)
|
||||
return await mock_strategy.mock(
|
||||
tool, args, tool_context, self._tool_connection_map, self._state_store
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright 2026 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 Any
|
||||
from typing import Awaitable
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from google.adk.tools.agent_simulator.agent_simulator_config import AgentSimulatorConfig
|
||||
from google.adk.tools.agent_simulator.agent_simulator_engine import AgentSimulatorEngine
|
||||
from google.adk.tools.agent_simulator.agent_simulator_plugin import AgentSimulatorPlugin
|
||||
from google.adk.tools.base_tool import BaseTool
|
||||
|
||||
from ...utils.feature_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class AgentSimulatorFactory:
|
||||
"""Factory for creating AgentSimulator instances."""
|
||||
|
||||
@staticmethod
|
||||
def create_callback(
|
||||
config: AgentSimulatorConfig,
|
||||
) -> Callable[
|
||||
[BaseTool, Dict[str, Any], Any], Awaitable[Optional[Dict[str, Any]]]
|
||||
]:
|
||||
"""Creates a callback function for AgentSimulator.
|
||||
|
||||
Args:
|
||||
config: The configuration for the AgentSimulator.
|
||||
|
||||
Returns:
|
||||
A callable that can be used as a before_tool_callback or after_tool_callback.
|
||||
"""
|
||||
simulator_engine = AgentSimulatorEngine(config)
|
||||
|
||||
async def _agent_simulator_callback(
|
||||
tool: BaseTool, args: Dict[str, Any], tool_context: Any
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
return await simulator_engine.simulate(tool, args, tool_context)
|
||||
|
||||
return _agent_simulator_callback
|
||||
|
||||
@staticmethod
|
||||
def create_plugin(
|
||||
config: AgentSimulatorConfig,
|
||||
) -> AgentSimulatorPlugin:
|
||||
"""Creates an ADK Plugin for AgentSimulator.
|
||||
|
||||
Args:
|
||||
config: The configuration for the AgentSimulator.
|
||||
|
||||
Returns:
|
||||
An instance of AgentSimulatorPlugin that can be used as an ADK plugin.
|
||||
"""
|
||||
simulator_engine = AgentSimulatorEngine(config)
|
||||
return AgentSimulatorPlugin(simulator_engine)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright 2024 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 Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from google.adk.plugins import BasePlugin
|
||||
from google.adk.tools.agent_simulator.agent_simulator_config import AgentSimulatorConfig
|
||||
from google.adk.tools.agent_simulator.agent_simulator_engine import AgentSimulatorEngine
|
||||
from google.adk.tools.base_tool import BaseTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
|
||||
class AgentSimulatorPlugin(BasePlugin):
|
||||
"""ADK Plugin for AgentSimulator."""
|
||||
|
||||
name: str = "AgentSimulator"
|
||||
|
||||
def __init__(self, simulator_engine: AgentSimulatorEngine):
|
||||
self._simulator_engine = simulator_engine
|
||||
|
||||
async def before_tool_callback(
|
||||
self, tool: BaseTool, tool_args: dict[str, Any], tool_context: ToolContext
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Invokes the AgentSimulatorEngine before a tool call."""
|
||||
return await self._simulator_engine.simulate(tool, tool_args, tool_context)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2026 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.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright 2026 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 Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from google.adk.tools.agent_simulator.tool_connection_map import ToolConnectionMap
|
||||
|
||||
|
||||
class MockStrategy:
|
||||
"""Base class for mock strategies."""
|
||||
|
||||
async def mock(
|
||||
self,
|
||||
tool: BaseTool,
|
||||
args: Dict[str, Any],
|
||||
tool_context: Any,
|
||||
tool_connection_map: Optional[ToolConnectionMap],
|
||||
state_store: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Generates a mock response for a tool call."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class TracingMockStrategy(MockStrategy):
|
||||
"""Mocks a tool response based on tracing and an LLM."""
|
||||
|
||||
def __init__(
|
||||
self, llm_name: str, llm_config: genai_types.GenerateContentConfig
|
||||
):
|
||||
self._llm_name = llm_name
|
||||
self._llm_config = llm_config
|
||||
|
||||
async def mock(
|
||||
self,
|
||||
tool: BaseTool,
|
||||
args: Dict[str, Any],
|
||||
tool_context: Any,
|
||||
tool_connection_map: Optional[ToolConnectionMap],
|
||||
state_store: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
# TODO: Implement tracing LLM-based mocking.
|
||||
return {"status": "error", "error_message": "Not implemented"}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user