google-adk 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google/adk/agents/callback_context.py +2 -1
- google/adk/agents/readonly_context.py +3 -1
- google/adk/auth/auth_credential.py +4 -1
- google/adk/cli/browser/index.html +4 -4
- google/adk/cli/browser/{main-QOEMUXM4.js → main-PKDNKWJE.js} +59 -59
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
- google/adk/cli/cli.py +3 -2
- google/adk/cli/cli_eval.py +6 -85
- google/adk/cli/cli_tools_click.py +39 -10
- google/adk/cli/fast_api.py +53 -184
- google/adk/cli/utils/agent_loader.py +137 -0
- google/adk/cli/utils/cleanup.py +40 -0
- google/adk/cli/utils/evals.py +2 -1
- google/adk/cli/utils/logs.py +2 -7
- google/adk/code_executors/code_execution_utils.py +2 -1
- google/adk/code_executors/container_code_executor.py +0 -1
- google/adk/code_executors/vertex_ai_code_executor.py +6 -8
- google/adk/evaluation/eval_case.py +3 -1
- google/adk/evaluation/eval_metrics.py +74 -0
- google/adk/evaluation/eval_result.py +86 -0
- google/adk/evaluation/eval_set.py +2 -0
- google/adk/evaluation/eval_set_results_manager.py +47 -0
- google/adk/evaluation/eval_sets_manager.py +2 -1
- google/adk/evaluation/evaluator.py +2 -0
- google/adk/evaluation/local_eval_set_results_manager.py +113 -0
- google/adk/evaluation/local_eval_sets_manager.py +4 -4
- google/adk/evaluation/response_evaluator.py +2 -1
- google/adk/evaluation/trajectory_evaluator.py +3 -2
- google/adk/examples/base_example_provider.py +1 -0
- google/adk/flows/llm_flows/base_llm_flow.py +4 -6
- google/adk/flows/llm_flows/contents.py +3 -1
- google/adk/flows/llm_flows/instructions.py +7 -77
- google/adk/flows/llm_flows/single_flow.py +1 -1
- google/adk/models/base_llm.py +2 -1
- google/adk/models/base_llm_connection.py +2 -0
- google/adk/models/google_llm.py +4 -1
- google/adk/models/lite_llm.py +3 -2
- google/adk/models/llm_response.py +2 -1
- google/adk/runners.py +36 -4
- google/adk/sessions/_session_util.py +2 -1
- google/adk/sessions/database_session_service.py +5 -8
- google/adk/sessions/vertex_ai_session_service.py +28 -13
- google/adk/telemetry.py +4 -2
- google/adk/tools/agent_tool.py +1 -1
- google/adk/tools/apihub_tool/apihub_toolset.py +1 -1
- google/adk/tools/apihub_tool/clients/apihub_client.py +10 -3
- google/adk/tools/apihub_tool/clients/secret_client.py +1 -0
- google/adk/tools/application_integration_tool/application_integration_toolset.py +6 -2
- google/adk/tools/application_integration_tool/clients/connections_client.py +8 -1
- google/adk/tools/application_integration_tool/clients/integration_client.py +3 -1
- google/adk/tools/application_integration_tool/integration_connector_tool.py +1 -1
- google/adk/tools/base_toolset.py +40 -2
- google/adk/tools/bigquery/__init__.py +28 -0
- google/adk/tools/bigquery/bigquery_credentials.py +216 -0
- google/adk/tools/bigquery/bigquery_tool.py +116 -0
- google/adk/tools/function_parameter_parse_util.py +7 -0
- google/adk/tools/function_tool.py +33 -3
- google/adk/tools/get_user_choice_tool.py +1 -0
- google/adk/tools/google_api_tool/__init__.py +17 -11
- google/adk/tools/google_api_tool/google_api_tool.py +1 -1
- google/adk/tools/google_api_tool/google_api_toolset.py +0 -14
- google/adk/tools/google_api_tool/google_api_toolsets.py +8 -2
- google/adk/tools/google_search_tool.py +2 -2
- google/adk/tools/mcp_tool/conversion_utils.py +6 -2
- google/adk/tools/mcp_tool/mcp_session_manager.py +62 -188
- google/adk/tools/mcp_tool/mcp_tool.py +27 -24
- google/adk/tools/mcp_tool/mcp_toolset.py +76 -131
- google/adk/tools/openapi_tool/auth/credential_exchangers/base_credential_exchanger.py +1 -3
- google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py +6 -7
- google/adk/tools/openapi_tool/common/common.py +5 -1
- google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +7 -2
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +2 -7
- google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +5 -1
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +11 -1
- google/adk/tools/toolbox_toolset.py +31 -3
- google/adk/utils/__init__.py +13 -0
- google/adk/utils/instructions_utils.py +131 -0
- google/adk/version.py +1 -1
- {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/METADATA +12 -15
- {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/RECORD +83 -78
- google/adk/agents/base_agent.py.orig +0 -330
- google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -18
- google/adk/cli/fast_api.py.orig +0 -822
- google/adk/memory/base_memory_service.py.orig +0 -76
- google/adk/models/google_llm.py.orig +0 -305
- google/adk/tools/_built_in_code_execution_tool.py +0 -70
- google/adk/tools/mcp_tool/mcp_session_manager.py.orig +0 -322
- {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/WHEEL +0 -0
- {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/entry_points.txt +0 -0
- {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -12,19 +12,13 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
import asyncio
|
16
|
-
from contextlib import AsyncExitStack
|
17
15
|
import logging
|
18
|
-
import os
|
19
|
-
import signal
|
20
16
|
import sys
|
21
17
|
from typing import List
|
22
18
|
from typing import Optional
|
23
19
|
from typing import TextIO
|
24
20
|
from typing import Union
|
25
21
|
|
26
|
-
from typing_extensions import override
|
27
|
-
|
28
22
|
from ...agents.readonly_context import ReadonlyContext
|
29
23
|
from ..base_tool import BaseTool
|
30
24
|
from ..base_toolset import BaseToolset
|
@@ -36,7 +30,6 @@ from .mcp_session_manager import SseServerParams
|
|
36
30
|
# Attempt to import MCP Tool from the MCP library, and hints user to upgrade
|
37
31
|
# their Python version to 3.10 if it fails.
|
38
32
|
try:
|
39
|
-
from mcp import ClientSession
|
40
33
|
from mcp import StdioServerParameters
|
41
34
|
from mcp.types import ListToolsResult
|
42
35
|
except ImportError as e:
|
@@ -58,16 +51,31 @@ logger = logging.getLogger("google_adk." + __name__)
|
|
58
51
|
class MCPToolset(BaseToolset):
|
59
52
|
"""Connects to a MCP Server, and retrieves MCP Tools into ADK Tools.
|
60
53
|
|
54
|
+
This toolset manages the connection to an MCP server and provides tools
|
55
|
+
that can be used by an agent. It properly implements the BaseToolset
|
56
|
+
interface for easy integration with the agent framework.
|
57
|
+
|
61
58
|
Usage:
|
62
|
-
```
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
)
|
59
|
+
```python
|
60
|
+
toolset = MCPToolset(
|
61
|
+
connection_params=StdioServerParameters(
|
62
|
+
command='npx',
|
63
|
+
args=["-y", "@modelcontextprotocol/server-filesystem"],
|
64
|
+
),
|
65
|
+
tool_filter=['read_file', 'list_directory'] # Optional: filter specific tools
|
70
66
|
)
|
67
|
+
|
68
|
+
# Use in an agent
|
69
|
+
agent = LlmAgent(
|
70
|
+
model='gemini-2.0-flash',
|
71
|
+
name='enterprise_assistant',
|
72
|
+
instruction='Help user accessing their file systems',
|
73
|
+
tools=[toolset],
|
74
|
+
)
|
75
|
+
|
76
|
+
# Cleanup is handled automatically by the agent framework
|
77
|
+
# But you can also manually close if needed:
|
78
|
+
# await toolset.close()
|
71
79
|
```
|
72
80
|
"""
|
73
81
|
|
@@ -75,152 +83,89 @@ class MCPToolset(BaseToolset):
|
|
75
83
|
self,
|
76
84
|
*,
|
77
85
|
connection_params: StdioServerParameters | SseServerParams,
|
78
|
-
errlog: TextIO = sys.stderr,
|
79
86
|
tool_filter: Optional[Union[ToolPredicate, List[str]]] = None,
|
87
|
+
errlog: TextIO = sys.stderr,
|
80
88
|
):
|
81
89
|
"""Initializes the MCPToolset.
|
82
90
|
|
83
91
|
Args:
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
92
|
+
connection_params: The connection parameters to the MCP server. Can be:
|
93
|
+
`StdioServerParameters` for using local mcp server (e.g. using `npx` or
|
94
|
+
`python3`); or `SseServerParams` for a local/remote SSE server.
|
95
|
+
tool_filter: Optional filter to select specific tools. Can be either:
|
96
|
+
- A list of tool names to include
|
97
|
+
- A ToolPredicate function for custom filtering logic
|
98
|
+
errlog: TextIO stream for error logging.
|
89
99
|
"""
|
100
|
+
super().__init__(tool_filter=tool_filter)
|
90
101
|
|
91
102
|
if not connection_params:
|
92
103
|
raise ValueError("Missing connection params in MCPToolset.")
|
104
|
+
|
93
105
|
self._connection_params = connection_params
|
94
106
|
self._errlog = errlog
|
95
|
-
self._exit_stack = AsyncExitStack()
|
96
|
-
self._creator_task_id = None
|
97
|
-
self._process_pid = None # Store the subprocess PID
|
98
107
|
|
99
|
-
|
108
|
+
# Create the session manager that will handle the MCP connection
|
109
|
+
self._mcp_session_manager = MCPSessionManager(
|
100
110
|
connection_params=self._connection_params,
|
101
|
-
exit_stack=self._exit_stack,
|
102
111
|
errlog=self._errlog,
|
103
112
|
)
|
104
113
|
self._session = None
|
105
|
-
self.tool_filter = tool_filter
|
106
|
-
self._initialized = False
|
107
|
-
|
108
|
-
async def _initialize(self) -> ClientSession:
|
109
|
-
"""Connects to the MCP Server and initializes the ClientSession."""
|
110
|
-
# Store the current task ID when initializing
|
111
|
-
self._creator_task_id = id(asyncio.current_task())
|
112
|
-
self._session, process = await self._session_manager.create_session()
|
113
|
-
# Store the process PID if available
|
114
|
-
if process and hasattr(process, "pid"):
|
115
|
-
self._process_pid = process.pid
|
116
|
-
self._initialized = True
|
117
|
-
return self._session
|
118
|
-
|
119
|
-
def _is_selected(
|
120
|
-
self, tool: BaseTool, readonly_context: Optional[ReadonlyContext]
|
121
|
-
) -> bool:
|
122
|
-
"""Checks if a tool should be selected based on the tool filter."""
|
123
|
-
if self.tool_filter is None:
|
124
|
-
return True
|
125
|
-
if isinstance(self.tool_filter, ToolPredicate):
|
126
|
-
return self.tool_filter(tool, readonly_context)
|
127
|
-
if isinstance(self.tool_filter, list):
|
128
|
-
return tool.name in self.tool_filter
|
129
|
-
return False
|
130
|
-
|
131
|
-
@override
|
132
|
-
async def close(self):
|
133
|
-
"""Safely closes the connection to MCP Server with guaranteed resource cleanup."""
|
134
|
-
if not self._initialized:
|
135
|
-
return # Nothing to close
|
136
|
-
|
137
|
-
logger.info("Closing MCP Toolset")
|
138
|
-
|
139
|
-
# Step 1: Try graceful shutdown of the session if it exists
|
140
|
-
if self._session:
|
141
|
-
try:
|
142
|
-
logger.info("Attempting graceful session shutdown")
|
143
|
-
await self._session.shutdown()
|
144
|
-
except Exception as e:
|
145
|
-
logger.warning(f"Session shutdown error (continuing cleanup): {e}")
|
146
|
-
|
147
|
-
# Step 2: Try to close the exit stack
|
148
|
-
try:
|
149
|
-
logger.info("Closing AsyncExitStack")
|
150
|
-
await self._exit_stack.aclose()
|
151
|
-
# If we get here, the exit stack closed successfully
|
152
|
-
logger.info("AsyncExitStack closed successfully")
|
153
|
-
return
|
154
|
-
except RuntimeError as e:
|
155
|
-
if "Attempted to exit cancel scope in a different task" in str(e):
|
156
|
-
logger.warning("Task mismatch during shutdown - using fallback cleanup")
|
157
|
-
# Continue to manual cleanup
|
158
|
-
else:
|
159
|
-
logger.error(f"Unexpected RuntimeError: {e}")
|
160
|
-
# Continue to manual cleanup
|
161
|
-
except Exception as e:
|
162
|
-
logger.error(f"Error during exit stack closure: {e}")
|
163
|
-
# Continue to manual cleanup
|
164
|
-
|
165
|
-
# Step 3: Manual cleanup of the subprocess if we have its PID
|
166
|
-
if self._process_pid:
|
167
|
-
await self._ensure_process_terminated(self._process_pid)
|
168
|
-
|
169
|
-
# Step 4: Ask the session manager to do any additional cleanup it can
|
170
|
-
await self._session_manager._emergency_cleanup()
|
171
|
-
|
172
|
-
async def _ensure_process_terminated(self, pid):
|
173
|
-
"""Ensure a process is terminated using its PID."""
|
174
|
-
try:
|
175
|
-
# Check if process exists
|
176
|
-
os.kill(pid, 0) # This just checks if the process exists
|
177
|
-
|
178
|
-
logger.info(f"Terminating process with PID {pid}")
|
179
|
-
# First try SIGTERM for graceful shutdown
|
180
|
-
os.kill(pid, signal.SIGTERM)
|
181
|
-
|
182
|
-
# Give it a moment to terminate
|
183
|
-
for _ in range(30): # wait up to 3 seconds
|
184
|
-
await asyncio.sleep(0.1)
|
185
|
-
try:
|
186
|
-
os.kill(pid, 0) # Process still exists
|
187
|
-
except ProcessLookupError:
|
188
|
-
logger.info(f"Process {pid} terminated successfully")
|
189
|
-
return
|
190
|
-
|
191
|
-
# If we get here, process didn't terminate gracefully
|
192
|
-
logger.warning(
|
193
|
-
f"Process {pid} didn't terminate gracefully, using SIGKILL"
|
194
|
-
)
|
195
|
-
os.kill(pid, signal.SIGKILL)
|
196
|
-
|
197
|
-
except ProcessLookupError:
|
198
|
-
logger.info(f"Process {pid} already terminated")
|
199
|
-
except Exception as e:
|
200
|
-
logger.error(f"Error terminating process {pid}: {e}")
|
201
114
|
|
202
|
-
@retry_on_closed_resource("
|
203
|
-
@override
|
115
|
+
@retry_on_closed_resource("_reinitialize_session")
|
204
116
|
async def get_tools(
|
205
117
|
self,
|
206
118
|
readonly_context: Optional[ReadonlyContext] = None,
|
207
|
-
) -> List[
|
208
|
-
"""
|
119
|
+
) -> List[BaseTool]:
|
120
|
+
"""Return all tools in the toolset based on the provided context.
|
121
|
+
|
122
|
+
Args:
|
123
|
+
readonly_context: Context used to filter tools available to the agent.
|
124
|
+
If None, all tools in the toolset are returned.
|
209
125
|
|
210
126
|
Returns:
|
211
|
-
|
127
|
+
List[BaseTool]: A list of tools available under the specified context.
|
212
128
|
"""
|
129
|
+
# Get session from session manager
|
213
130
|
if not self._session:
|
214
|
-
await self.
|
131
|
+
self._session = await self._mcp_session_manager.create_session()
|
132
|
+
|
133
|
+
# Fetch available tools from the MCP server
|
215
134
|
tools_response: ListToolsResult = await self._session.list_tools()
|
135
|
+
|
136
|
+
# Apply filtering based on context and tool_filter
|
216
137
|
tools = []
|
217
138
|
for tool in tools_response.tools:
|
218
139
|
mcp_tool = MCPTool(
|
219
140
|
mcp_tool=tool,
|
220
|
-
|
221
|
-
mcp_session_manager=self._session_manager,
|
141
|
+
mcp_session_manager=self._mcp_session_manager,
|
222
142
|
)
|
223
143
|
|
224
|
-
if self.
|
144
|
+
if self._is_tool_selected(mcp_tool, readonly_context):
|
225
145
|
tools.append(mcp_tool)
|
226
146
|
return tools
|
147
|
+
|
148
|
+
async def _reinitialize_session(self):
|
149
|
+
"""Reinitializes the session when connection is lost."""
|
150
|
+
# Close the old session and clear cache
|
151
|
+
await self._mcp_session_manager.close()
|
152
|
+
self._session = await self._mcp_session_manager.create_session()
|
153
|
+
|
154
|
+
# Tools will be reloaded on next get_tools call
|
155
|
+
|
156
|
+
async def close(self) -> None:
|
157
|
+
"""Performs cleanup and releases resources held by the toolset.
|
158
|
+
|
159
|
+
This method closes the MCP session and cleans up all associated resources.
|
160
|
+
It's designed to be safe to call multiple times and handles cleanup errors
|
161
|
+
gracefully to avoid blocking application shutdown.
|
162
|
+
"""
|
163
|
+
try:
|
164
|
+
await self._mcp_session_manager.close()
|
165
|
+
except Exception as e:
|
166
|
+
# Log the error but don't re-raise to avoid blocking shutdown
|
167
|
+
print(f"Warning: Error during MCPToolset cleanup: {e}", file=self._errlog)
|
168
|
+
finally:
|
169
|
+
# Clear cached tools
|
170
|
+
self._tools_cache = None
|
171
|
+
self._tools_loaded = False
|
@@ -21,14 +21,13 @@ from google.auth.transport.requests import Request
|
|
21
21
|
from google.oauth2 import service_account
|
22
22
|
import google.oauth2.credentials
|
23
23
|
|
24
|
-
from .....auth.auth_credential import
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
HttpCredentials,
|
29
|
-
)
|
24
|
+
from .....auth.auth_credential import AuthCredential
|
25
|
+
from .....auth.auth_credential import AuthCredentialTypes
|
26
|
+
from .....auth.auth_credential import HttpAuth
|
27
|
+
from .....auth.auth_credential import HttpCredentials
|
30
28
|
from .....auth.auth_schemes import AuthScheme
|
31
|
-
from .base_credential_exchanger import AuthCredentialMissingError
|
29
|
+
from .base_credential_exchanger import AuthCredentialMissingError
|
30
|
+
from .base_credential_exchanger import BaseAuthCredentialExchanger
|
32
31
|
|
33
32
|
|
34
33
|
class ServiceAccountCredentialExchanger(BaseAuthCredentialExchanger):
|
@@ -14,7 +14,11 @@
|
|
14
14
|
|
15
15
|
import keyword
|
16
16
|
import re
|
17
|
-
from typing import Any
|
17
|
+
from typing import Any
|
18
|
+
from typing import Dict
|
19
|
+
from typing import List
|
20
|
+
from typing import Optional
|
21
|
+
from typing import Union
|
18
22
|
|
19
23
|
from fastapi.openapi.models import Response
|
20
24
|
from fastapi.openapi.models import Schema
|
@@ -12,10 +12,15 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
from .openapi_spec_parser import OpenApiSpecParser
|
15
|
+
from .openapi_spec_parser import OpenApiSpecParser
|
16
|
+
from .openapi_spec_parser import OperationEndpoint
|
17
|
+
from .openapi_spec_parser import ParsedOperation
|
16
18
|
from .openapi_toolset import OpenAPIToolset
|
17
19
|
from .operation_parser import OperationParser
|
18
|
-
from .rest_api_tool import AuthPreparationState
|
20
|
+
from .rest_api_tool import AuthPreparationState
|
21
|
+
from .rest_api_tool import RestApiTool
|
22
|
+
from .rest_api_tool import snake_to_lower_camel
|
23
|
+
from .rest_api_tool import to_gemini_schema
|
19
24
|
from .tool_auth_handler import ToolAuthHandler
|
20
25
|
|
21
26
|
__all__ = [
|
@@ -103,12 +103,12 @@ class OpenAPIToolset(BaseToolset):
|
|
103
103
|
tool_filter: The filter used to filter the tools in the toolset. It can be
|
104
104
|
either a tool predicate or a list of tool names of the tools to expose.
|
105
105
|
"""
|
106
|
+
super().__init__(tool_filter=tool_filter)
|
106
107
|
if not spec_dict:
|
107
108
|
spec_dict = self._load_spec(spec_str, spec_str_type)
|
108
109
|
self._tools: Final[List[RestApiTool]] = list(self._parse(spec_dict))
|
109
110
|
if auth_scheme or auth_credential:
|
110
111
|
self._configure_auth_all(auth_scheme, auth_credential)
|
111
|
-
self.tool_filter = tool_filter
|
112
112
|
|
113
113
|
def _configure_auth_all(
|
114
114
|
self, auth_scheme: AuthScheme, auth_credential: AuthCredential
|
@@ -129,12 +129,7 @@ class OpenAPIToolset(BaseToolset):
|
|
129
129
|
return [
|
130
130
|
tool
|
131
131
|
for tool in self._tools
|
132
|
-
if self.
|
133
|
-
or (
|
134
|
-
self.tool_filter(tool, readonly_context)
|
135
|
-
if isinstance(self.tool_filter, ToolPredicate)
|
136
|
-
else tool.name in self.tool_filter
|
137
|
-
)
|
132
|
+
if self._is_tool_selected(tool, readonly_context)
|
138
133
|
]
|
139
134
|
|
140
135
|
def get_tool(self, tool_name: str) -> Optional[RestApiTool]:
|
@@ -14,7 +14,11 @@
|
|
14
14
|
|
15
15
|
import inspect
|
16
16
|
from textwrap import dedent
|
17
|
-
from typing import Any
|
17
|
+
from typing import Any
|
18
|
+
from typing import Dict
|
19
|
+
from typing import List
|
20
|
+
from typing import Optional
|
21
|
+
from typing import Union
|
18
22
|
|
19
23
|
from fastapi.encoders import jsonable_encoder
|
20
24
|
from fastapi.openapi.models import Operation
|
@@ -41,6 +41,16 @@ from .openapi_spec_parser import ParsedOperation
|
|
41
41
|
from .operation_parser import OperationParser
|
42
42
|
from .tool_auth_handler import ToolAuthHandler
|
43
43
|
|
44
|
+
# Not supported by the Gemini API
|
45
|
+
_OPENAPI_SCHEMA_IGNORE_FIELDS = (
|
46
|
+
"title",
|
47
|
+
"default",
|
48
|
+
"format",
|
49
|
+
"additional_properties",
|
50
|
+
"ref",
|
51
|
+
"def",
|
52
|
+
)
|
53
|
+
|
44
54
|
|
45
55
|
def snake_to_lower_camel(snake_case_string: str):
|
46
56
|
"""Converts a snake_case string to a lower_camel_case string.
|
@@ -121,7 +131,7 @@ def to_gemini_schema(openapi_schema: Optional[Dict[str, Any]] = None) -> Schema:
|
|
121
131
|
snake_case_key = to_snake_case(key)
|
122
132
|
# Check if the snake_case_key exists in the Schema model's fields.
|
123
133
|
if snake_case_key in Schema.model_fields:
|
124
|
-
if snake_case_key in
|
134
|
+
if snake_case_key in _OPENAPI_SCHEMA_IGNORE_FIELDS:
|
125
135
|
# Ignore these fields as Gemini backend doesn't recognize them, and will
|
126
136
|
# throw exception if they appear in the schema.
|
127
137
|
# Format: properties[expiration].format: only 'enum' and 'date-time' are
|
@@ -12,8 +12,12 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
+
from typing import Any
|
16
|
+
from typing import Callable
|
15
17
|
from typing import List
|
18
|
+
from typing import Mapping
|
16
19
|
from typing import Optional
|
20
|
+
from typing import Union
|
17
21
|
|
18
22
|
import toolbox_core as toolbox
|
19
23
|
from typing_extensions import override
|
@@ -40,12 +44,24 @@ class ToolboxToolset(BaseToolset):
|
|
40
44
|
server_url: str,
|
41
45
|
toolset_name: Optional[str] = None,
|
42
46
|
tool_names: Optional[List[str]] = None,
|
47
|
+
auth_token_getters: Optional[dict[str, Callable[[], str]]] = None,
|
48
|
+
bound_params: Optional[
|
49
|
+
Mapping[str, Union[Callable[[], Any], Any]]
|
50
|
+
] = None,
|
43
51
|
):
|
44
52
|
"""Args:
|
45
53
|
|
46
54
|
server_url: The URL of the toolbox server.
|
47
55
|
toolset_name: The name of the toolbox toolset to load.
|
48
56
|
tool_names: The names of the tools to load.
|
57
|
+
auth_token_getters: A mapping of authentication service names to
|
58
|
+
callables that return the corresponding authentication token. see:
|
59
|
+
https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-core#authenticating-tools
|
60
|
+
for details.
|
61
|
+
bound_params: A mapping of parameter names to bind to specific values or
|
62
|
+
callables that are called to produce values as needed. see:
|
63
|
+
https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-core#binding-parameter-values
|
64
|
+
for details.
|
49
65
|
The resulting ToolboxToolset will contain both tools loaded by tool_names
|
50
66
|
and toolset_name.
|
51
67
|
"""
|
@@ -53,9 +69,11 @@ class ToolboxToolset(BaseToolset):
|
|
53
69
|
raise ValueError("tool_names and toolset_name cannot both be None")
|
54
70
|
super().__init__()
|
55
71
|
self._server_url = server_url
|
56
|
-
self._toolbox_client = toolbox.
|
72
|
+
self._toolbox_client = toolbox.ToolboxClient(server_url)
|
57
73
|
self._toolset_name = toolset_name
|
58
74
|
self._tool_names = tool_names
|
75
|
+
self._auth_token_getters = auth_token_getters or {}
|
76
|
+
self._bound_params = bound_params or {}
|
59
77
|
|
60
78
|
@override
|
61
79
|
async def get_tools(
|
@@ -65,11 +83,21 @@ class ToolboxToolset(BaseToolset):
|
|
65
83
|
if self._toolset_name:
|
66
84
|
tools.extend([
|
67
85
|
FunctionTool(tool)
|
68
|
-
for tool in self._toolbox_client.load_toolset(
|
86
|
+
for tool in await self._toolbox_client.load_toolset(
|
87
|
+
self._toolset_name,
|
88
|
+
auth_token_getters=self._auth_token_getters,
|
89
|
+
bound_params=self._bound_params,
|
90
|
+
)
|
69
91
|
])
|
70
92
|
if self._tool_names:
|
71
93
|
tools.extend([
|
72
|
-
FunctionTool(
|
94
|
+
FunctionTool(
|
95
|
+
await self._toolbox_client.load_tool(
|
96
|
+
tool_name,
|
97
|
+
auth_token_getters=self._auth_token_getters,
|
98
|
+
bound_params=self._bound_params,
|
99
|
+
)
|
100
|
+
)
|
73
101
|
for tool_name in self._tool_names
|
74
102
|
])
|
75
103
|
return tools
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copyright 2025 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.
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# Copyright 2025 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
|
+
import re
|
16
|
+
|
17
|
+
from ..agents.readonly_context import ReadonlyContext
|
18
|
+
from ..sessions.state import State
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
'inject_session_state',
|
22
|
+
]
|
23
|
+
|
24
|
+
|
25
|
+
async def inject_session_state(
|
26
|
+
template: str,
|
27
|
+
readonly_context: ReadonlyContext,
|
28
|
+
) -> str:
|
29
|
+
"""Populates values in the instruction template, e.g. state, artifact, etc.
|
30
|
+
|
31
|
+
This method is intended to be used in InstructionProvider based instruction
|
32
|
+
and global_instruction which are called with readonly_context.
|
33
|
+
|
34
|
+
e.g.
|
35
|
+
```
|
36
|
+
...
|
37
|
+
from google.adk.utils import instructions_utils
|
38
|
+
|
39
|
+
async def build_instruction(
|
40
|
+
readonly_context: ReadonlyContext,
|
41
|
+
) -> str:
|
42
|
+
return await instructions_utils.inject_session_state(
|
43
|
+
'You can inject a state variable like {var_name} or an artifact '
|
44
|
+
'{artifact.file_name} into the instruction template.',
|
45
|
+
readonly_context,
|
46
|
+
)
|
47
|
+
|
48
|
+
agent = Agent(
|
49
|
+
model="gemini-2.0-flash",
|
50
|
+
name="agent",
|
51
|
+
instruction=build_instruction,
|
52
|
+
)
|
53
|
+
```
|
54
|
+
|
55
|
+
Args:
|
56
|
+
template: The instruction template.
|
57
|
+
readonly_context: The read-only context
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
The instruction template with values populated.
|
61
|
+
"""
|
62
|
+
|
63
|
+
invocation_context = readonly_context._invocation_context
|
64
|
+
|
65
|
+
async def _async_sub(pattern, repl_async_fn, string) -> str:
|
66
|
+
result = []
|
67
|
+
last_end = 0
|
68
|
+
for match in re.finditer(pattern, string):
|
69
|
+
result.append(string[last_end : match.start()])
|
70
|
+
replacement = await repl_async_fn(match)
|
71
|
+
result.append(replacement)
|
72
|
+
last_end = match.end()
|
73
|
+
result.append(string[last_end:])
|
74
|
+
return ''.join(result)
|
75
|
+
|
76
|
+
async def _replace_match(match) -> str:
|
77
|
+
var_name = match.group().lstrip('{').rstrip('}').strip()
|
78
|
+
optional = False
|
79
|
+
if var_name.endswith('?'):
|
80
|
+
optional = True
|
81
|
+
var_name = var_name.removesuffix('?')
|
82
|
+
if var_name.startswith('artifact.'):
|
83
|
+
var_name = var_name.removeprefix('artifact.')
|
84
|
+
if invocation_context.artifact_service is None:
|
85
|
+
raise ValueError('Artifact service is not initialized.')
|
86
|
+
artifact = await invocation_context.artifact_service.load_artifact(
|
87
|
+
app_name=invocation_context.session.app_name,
|
88
|
+
user_id=invocation_context.session.user_id,
|
89
|
+
session_id=invocation_context.session.id,
|
90
|
+
filename=var_name,
|
91
|
+
)
|
92
|
+
if not var_name:
|
93
|
+
raise KeyError(f'Artifact {var_name} not found.')
|
94
|
+
return str(artifact)
|
95
|
+
else:
|
96
|
+
if not _is_valid_state_name(var_name):
|
97
|
+
return match.group()
|
98
|
+
if var_name in invocation_context.session.state:
|
99
|
+
return str(invocation_context.session.state[var_name])
|
100
|
+
else:
|
101
|
+
if optional:
|
102
|
+
return ''
|
103
|
+
else:
|
104
|
+
raise KeyError(f'Context variable not found: `{var_name}`.')
|
105
|
+
|
106
|
+
return await _async_sub(r'{+[^{}]*}+', _replace_match, template)
|
107
|
+
|
108
|
+
|
109
|
+
def _is_valid_state_name(var_name):
|
110
|
+
"""Checks if the variable name is a valid state name.
|
111
|
+
|
112
|
+
Valid state is either:
|
113
|
+
- Valid identifier
|
114
|
+
- <Valid prefix>:<Valid identifier>
|
115
|
+
All the others will just return as it is.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
var_name: The variable name to check.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
True if the variable name is a valid state name, False otherwise.
|
122
|
+
"""
|
123
|
+
parts = var_name.split(':')
|
124
|
+
if len(parts) == 1:
|
125
|
+
return var_name.isidentifier()
|
126
|
+
|
127
|
+
if len(parts) == 2:
|
128
|
+
prefixes = [State.APP_PREFIX, State.USER_PREFIX, State.TEMP_PREFIX]
|
129
|
+
if (parts[0] + ':') in prefixes:
|
130
|
+
return parts[1].isidentifier()
|
131
|
+
return False
|
google/adk/version.py
CHANGED