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.
Files changed (90) hide show
  1. google/adk/agents/callback_context.py +2 -1
  2. google/adk/agents/readonly_context.py +3 -1
  3. google/adk/auth/auth_credential.py +4 -1
  4. google/adk/cli/browser/index.html +4 -4
  5. google/adk/cli/browser/{main-QOEMUXM4.js → main-PKDNKWJE.js} +59 -59
  6. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  7. google/adk/cli/cli.py +3 -2
  8. google/adk/cli/cli_eval.py +6 -85
  9. google/adk/cli/cli_tools_click.py +39 -10
  10. google/adk/cli/fast_api.py +53 -184
  11. google/adk/cli/utils/agent_loader.py +137 -0
  12. google/adk/cli/utils/cleanup.py +40 -0
  13. google/adk/cli/utils/evals.py +2 -1
  14. google/adk/cli/utils/logs.py +2 -7
  15. google/adk/code_executors/code_execution_utils.py +2 -1
  16. google/adk/code_executors/container_code_executor.py +0 -1
  17. google/adk/code_executors/vertex_ai_code_executor.py +6 -8
  18. google/adk/evaluation/eval_case.py +3 -1
  19. google/adk/evaluation/eval_metrics.py +74 -0
  20. google/adk/evaluation/eval_result.py +86 -0
  21. google/adk/evaluation/eval_set.py +2 -0
  22. google/adk/evaluation/eval_set_results_manager.py +47 -0
  23. google/adk/evaluation/eval_sets_manager.py +2 -1
  24. google/adk/evaluation/evaluator.py +2 -0
  25. google/adk/evaluation/local_eval_set_results_manager.py +113 -0
  26. google/adk/evaluation/local_eval_sets_manager.py +4 -4
  27. google/adk/evaluation/response_evaluator.py +2 -1
  28. google/adk/evaluation/trajectory_evaluator.py +3 -2
  29. google/adk/examples/base_example_provider.py +1 -0
  30. google/adk/flows/llm_flows/base_llm_flow.py +4 -6
  31. google/adk/flows/llm_flows/contents.py +3 -1
  32. google/adk/flows/llm_flows/instructions.py +7 -77
  33. google/adk/flows/llm_flows/single_flow.py +1 -1
  34. google/adk/models/base_llm.py +2 -1
  35. google/adk/models/base_llm_connection.py +2 -0
  36. google/adk/models/google_llm.py +4 -1
  37. google/adk/models/lite_llm.py +3 -2
  38. google/adk/models/llm_response.py +2 -1
  39. google/adk/runners.py +36 -4
  40. google/adk/sessions/_session_util.py +2 -1
  41. google/adk/sessions/database_session_service.py +5 -8
  42. google/adk/sessions/vertex_ai_session_service.py +28 -13
  43. google/adk/telemetry.py +4 -2
  44. google/adk/tools/agent_tool.py +1 -1
  45. google/adk/tools/apihub_tool/apihub_toolset.py +1 -1
  46. google/adk/tools/apihub_tool/clients/apihub_client.py +10 -3
  47. google/adk/tools/apihub_tool/clients/secret_client.py +1 -0
  48. google/adk/tools/application_integration_tool/application_integration_toolset.py +6 -2
  49. google/adk/tools/application_integration_tool/clients/connections_client.py +8 -1
  50. google/adk/tools/application_integration_tool/clients/integration_client.py +3 -1
  51. google/adk/tools/application_integration_tool/integration_connector_tool.py +1 -1
  52. google/adk/tools/base_toolset.py +40 -2
  53. google/adk/tools/bigquery/__init__.py +28 -0
  54. google/adk/tools/bigquery/bigquery_credentials.py +216 -0
  55. google/adk/tools/bigquery/bigquery_tool.py +116 -0
  56. google/adk/tools/function_parameter_parse_util.py +7 -0
  57. google/adk/tools/function_tool.py +33 -3
  58. google/adk/tools/get_user_choice_tool.py +1 -0
  59. google/adk/tools/google_api_tool/__init__.py +17 -11
  60. google/adk/tools/google_api_tool/google_api_tool.py +1 -1
  61. google/adk/tools/google_api_tool/google_api_toolset.py +0 -14
  62. google/adk/tools/google_api_tool/google_api_toolsets.py +8 -2
  63. google/adk/tools/google_search_tool.py +2 -2
  64. google/adk/tools/mcp_tool/conversion_utils.py +6 -2
  65. google/adk/tools/mcp_tool/mcp_session_manager.py +62 -188
  66. google/adk/tools/mcp_tool/mcp_tool.py +27 -24
  67. google/adk/tools/mcp_tool/mcp_toolset.py +76 -131
  68. google/adk/tools/openapi_tool/auth/credential_exchangers/base_credential_exchanger.py +1 -3
  69. google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py +6 -7
  70. google/adk/tools/openapi_tool/common/common.py +5 -1
  71. google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +7 -2
  72. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +2 -7
  73. google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +5 -1
  74. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +11 -1
  75. google/adk/tools/toolbox_toolset.py +31 -3
  76. google/adk/utils/__init__.py +13 -0
  77. google/adk/utils/instructions_utils.py +131 -0
  78. google/adk/version.py +1 -1
  79. {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/METADATA +12 -15
  80. {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/RECORD +83 -78
  81. google/adk/agents/base_agent.py.orig +0 -330
  82. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -18
  83. google/adk/cli/fast_api.py.orig +0 -822
  84. google/adk/memory/base_memory_service.py.orig +0 -76
  85. google/adk/models/google_llm.py.orig +0 -305
  86. google/adk/tools/_built_in_code_execution_tool.py +0 -70
  87. google/adk/tools/mcp_tool/mcp_session_manager.py.orig +0 -322
  88. {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/WHEEL +0 -0
  89. {google_adk-1.0.0.dist-info → google_adk-1.1.0.dist-info}/entry_points.txt +0 -0
  90. {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
- root_agent = LlmAgent(
64
- tools=MCPToolset(
65
- connection_params=StdioServerParameters(
66
- command='npx',
67
- args=["-y", "@modelcontextprotocol/server-filesystem"],
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
- connection_params: The connection parameters to the MCP server. Can be:
85
- `StdioServerParameters` for using local mcp server (e.g. using `npx` or
86
- `python3`); or `SseServerParams` for a local/remote SSE server.
87
- errlog: (Optional) TextIO stream for error logging. Use only for
88
- initializing a local stdio MCP session.
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
- self._session_manager = MCPSessionManager(
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("_initialize")
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[MCPTool]:
208
- """Loads all tools from the MCP Server.
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
- A list of MCPTools imported from the MCP Server.
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._initialize()
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
- mcp_session=self._session,
221
- mcp_session_manager=self._session_manager,
141
+ mcp_session_manager=self._mcp_session_manager,
222
142
  )
223
143
 
224
- if self._is_selected(mcp_tool, readonly_context):
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
@@ -15,9 +15,7 @@
15
15
  import abc
16
16
  from typing import Optional
17
17
 
18
- from .....auth.auth_credential import (
19
- AuthCredential,
20
- )
18
+ from .....auth.auth_credential import AuthCredential
21
19
  from .....auth.auth_schemes import AuthScheme
22
20
 
23
21
 
@@ -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
- AuthCredential,
26
- AuthCredentialTypes,
27
- HttpAuth,
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, BaseAuthCredentialExchanger
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, Dict, List, Optional, Union
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, OperationEndpoint, ParsedOperation
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, RestApiTool, snake_to_lower_camel, to_gemini_schema
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.tool_filter is None
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, Dict, List, Optional, Union
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 ["title", "default", "format"]:
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.ToolboxSyncClient(server_url)
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(self._toolset_name)
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(self._toolbox_client.load_tool(tool_name))
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
@@ -13,4 +13,4 @@
13
13
  # limitations under the License.
14
14
 
15
15
  # version: date+base_cl
16
- __version__ = "1.0.0"
16
+ __version__ = "1.1.0"