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,8 +12,7 @@
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 asynccontextmanager
15
+
17
16
  from contextlib import AsyncExitStack
18
17
  import functools
19
18
  import logging
@@ -71,29 +70,27 @@ def retry_on_closed_resource(async_reinit_func_name: str):
71
70
 
72
71
  Usage:
73
72
  class MCPTool:
74
- ...
75
- async def create_session(self):
76
- self.session = ...
73
+ ...
74
+ async def create_session(self):
75
+ self.session = ...
77
76
 
78
- @retry_on_closed_resource('create_session')
79
- async def use_session(self):
80
- await self.session.call_tool()
77
+ @retry_on_closed_resource('create_session')
78
+ async def use_session(self):
79
+ await self.session.call_tool()
81
80
 
82
81
  Args:
83
- async_reinit_func_name: The name of the async function to recreate session.
82
+ async_reinit_func_name: The name of the async function to recreate session.
84
83
 
85
84
  Returns:
86
- The decorated function.
85
+ The decorated function.
87
86
  """
88
87
 
89
88
  def decorator(func):
90
- @functools.wraps(
91
- func
92
- ) # Preserves original function metadata (name, docstring)
89
+ @functools.wraps(func) # Preserves original function metadata
93
90
  async def wrapper(self, *args, **kwargs):
94
91
  try:
95
92
  return await func(self, *args, **kwargs)
96
- except anyio.ClosedResourceError:
93
+ except anyio.ClosedResourceError as close_err:
97
94
  try:
98
95
  if hasattr(self, async_reinit_func_name) and callable(
99
96
  getattr(self, async_reinit_func_name)
@@ -105,7 +102,7 @@ def retry_on_closed_resource(async_reinit_func_name: str):
105
102
  f'Function {async_reinit_func_name} does not exist in decorated'
106
103
  ' class. Please check the function name in'
107
104
  ' retry_on_closed_resource decorator.'
108
- )
105
+ ) from close_err
109
106
  except Exception as reinit_err:
110
107
  raise RuntimeError(
111
108
  f'Error reinitializing: {reinit_err}'
@@ -117,45 +114,6 @@ def retry_on_closed_resource(async_reinit_func_name: str):
117
114
  return decorator
118
115
 
119
116
 
120
- @asynccontextmanager
121
- async def tracked_stdio_client(server, errlog, process=None):
122
- """A wrapper around stdio_client that ensures proper process tracking and cleanup."""
123
- our_process = process
124
-
125
- # If no process was provided, create one
126
- if our_process is None:
127
- our_process = await asyncio.create_subprocess_exec(
128
- server.command,
129
- *server.args,
130
- stdin=asyncio.subprocess.PIPE,
131
- stdout=asyncio.subprocess.PIPE,
132
- stderr=errlog,
133
- )
134
-
135
- # Use the original stdio_client, but ensure process cleanup
136
- try:
137
- async with stdio_client(server=server, errlog=errlog) as client:
138
- yield client, our_process
139
- finally:
140
- # Ensure the process is properly terminated if it still exists
141
- if our_process and our_process.returncode is None:
142
- try:
143
- logger.info(
144
- f'Terminating process {our_process.pid} from tracked_stdio_client'
145
- )
146
- our_process.terminate()
147
- try:
148
- await asyncio.wait_for(our_process.wait(), timeout=3.0)
149
- except asyncio.TimeoutError:
150
- # Force kill if it doesn't terminate quickly
151
- if our_process.returncode is None:
152
- logger.warning(f'Forcing kill of process {our_process.pid}')
153
- our_process.kill()
154
- except ProcessLookupError:
155
- # Process already gone, that's fine
156
- logger.info(f'Process {our_process.pid} already terminated')
157
-
158
-
159
117
  class MCPSessionManager:
160
118
  """Manages MCP client sessions.
161
119
 
@@ -166,162 +124,78 @@ class MCPSessionManager:
166
124
  def __init__(
167
125
  self,
168
126
  connection_params: StdioServerParameters | SseServerParams,
169
- exit_stack: AsyncExitStack,
170
127
  errlog: TextIO = sys.stderr,
171
128
  ):
172
129
  """Initializes the MCP session manager.
173
130
 
174
- Example usage:
175
- ```
176
- mcp_session_manager = MCPSessionManager(
177
- connection_params=connection_params,
178
- exit_stack=exit_stack,
179
- )
180
- session = await mcp_session_manager.create_session()
181
- ```
182
-
183
131
  Args:
184
132
  connection_params: Parameters for the MCP connection (Stdio or SSE).
185
- exit_stack: AsyncExitStack to manage the session lifecycle.
186
133
  errlog: (Optional) TextIO stream for error logging. Use only for
187
134
  initializing a local stdio MCP session.
188
135
  """
189
-
190
136
  self._connection_params = connection_params
191
- self._exit_stack = exit_stack
192
137
  self._errlog = errlog
193
- self._process = None # Track the subprocess
194
- self._active_processes = set() # Track all processes created
195
- self._active_file_handles = set() # Track file handles
138
+ # Each session manager maintains its own exit stack for proper cleanup
139
+ self._exit_stack: Optional[AsyncExitStack] = None
140
+ self._session: Optional[ClientSession] = None
196
141
 
197
- async def create_session(
198
- self,
199
- ) -> tuple[ClientSession, Optional[asyncio.subprocess.Process]]:
200
- """Creates a new MCP session and tracks the associated process."""
201
- session, process = await self._initialize_session(
202
- connection_params=self._connection_params,
203
- exit_stack=self._exit_stack,
204
- errlog=self._errlog,
205
- )
206
- self._process = process # Store reference to process
207
-
208
- # Track the process
209
- if process:
210
- self._active_processes.add(process)
211
-
212
- return session, process
213
-
214
- @classmethod
215
- async def _initialize_session(
216
- cls,
217
- *,
218
- connection_params: StdioServerParameters | SseServerParams,
219
- exit_stack: AsyncExitStack,
220
- errlog: TextIO = sys.stderr,
221
- ) -> tuple[ClientSession, Optional[asyncio.subprocess.Process]]:
222
- """Initializes an MCP client session.
223
-
224
- Args:
225
- connection_params: Parameters for the MCP connection (Stdio or SSE).
226
- exit_stack: AsyncExitStack to manage the session lifecycle.
227
- errlog: (Optional) TextIO stream for error logging. Use only for
228
- initializing a local stdio MCP session.
142
+ async def create_session(self) -> ClientSession:
143
+ """Creates and initializes an MCP client session.
229
144
 
230
145
  Returns:
231
146
  ClientSession: The initialized MCP client session.
232
147
  """
233
- process = None
234
-
235
- if isinstance(connection_params, StdioServerParameters):
236
- # For stdio connections, we need to track the subprocess
237
- client, process = await cls._create_stdio_client(
238
- server=connection_params,
239
- errlog=errlog,
240
- exit_stack=exit_stack,
241
- )
242
- elif isinstance(connection_params, SseServerParams):
243
- # For SSE connections, create the client without a subprocess
244
- client = sse_client(
245
- url=connection_params.url,
246
- headers=connection_params.headers,
247
- timeout=connection_params.timeout,
248
- sse_read_timeout=connection_params.sse_read_timeout,
249
- )
250
- else:
251
- raise ValueError(
252
- 'Unable to initialize connection. Connection should be'
253
- ' StdioServerParameters or SseServerParams, but got'
254
- f' {connection_params}'
255
- )
148
+ if self._session is not None:
149
+ return self._session
256
150
 
257
- # Create the session with the client
258
- transports = await exit_stack.enter_async_context(client)
259
- session = await exit_stack.enter_async_context(ClientSession(*transports))
260
- await session.initialize()
151
+ # Create a new exit stack for this session
152
+ self._exit_stack = AsyncExitStack()
261
153
 
262
- return session, process
263
-
264
- @staticmethod
265
- async def _create_stdio_client(
266
- server: StdioServerParameters,
267
- errlog: TextIO,
268
- exit_stack: AsyncExitStack,
269
- ) -> tuple[Any, asyncio.subprocess.Process]:
270
- """Create stdio client and return both the client and process.
271
-
272
- This implementation adapts to how the MCP stdio_client is created.
273
- The actual implementation may need to be adjusted based on the MCP library
274
- structure.
275
- """
276
- # Create the subprocess directly so we can track it
277
- process = await asyncio.create_subprocess_exec(
278
- server.command,
279
- *server.args,
280
- stdin=asyncio.subprocess.PIPE,
281
- stdout=asyncio.subprocess.PIPE,
282
- stderr=errlog,
283
- )
284
-
285
- # Create the stdio client using the MCP library
286
154
  try:
287
- # Method 1: Try using the existing process if stdio_client supports it
288
- client = stdio_client(server=server, errlog=errlog, process=process)
289
- except TypeError:
290
- # Method 2: If the above doesn't work, let stdio_client create its own process
291
- # and we'll need to terminate both processes later
292
- logger.warning(
293
- 'Using stdio_client with its own process - may lead to duplicate'
294
- ' processes'
295
- )
296
- client = stdio_client(server=server, errlog=errlog)
155
+ if isinstance(self._connection_params, StdioServerParameters):
156
+ client = stdio_client(
157
+ server=self._connection_params, errlog=self._errlog
158
+ )
159
+ elif isinstance(self._connection_params, SseServerParams):
160
+ client = sse_client(
161
+ url=self._connection_params.url,
162
+ headers=self._connection_params.headers,
163
+ timeout=self._connection_params.timeout,
164
+ sse_read_timeout=self._connection_params.sse_read_timeout,
165
+ )
166
+ else:
167
+ raise ValueError(
168
+ 'Unable to initialize connection. Connection should be'
169
+ ' StdioServerParameters or SseServerParams, but got'
170
+ f' {self._connection_params}'
171
+ )
297
172
 
298
- return client, process
173
+ transports = await self._exit_stack.enter_async_context(client)
174
+ session = await self._exit_stack.enter_async_context(
175
+ ClientSession(*transports)
176
+ )
177
+ await session.initialize()
299
178
 
300
- async def _emergency_cleanup(self):
301
- """Perform emergency cleanup of resources when normal cleanup fails."""
302
- logger.info('Performing emergency cleanup of MCPSessionManager resources')
179
+ self._session = session
180
+ return session
303
181
 
304
- # Clean up any tracked processes
305
- for proc in list(self._active_processes):
306
- try:
307
- if proc and proc.returncode is None:
308
- logger.info(f'Emergency termination of process {proc.pid}')
309
- proc.terminate()
310
- try:
311
- await asyncio.wait_for(proc.wait(), timeout=1.0)
312
- except asyncio.TimeoutError:
313
- logger.warning(f"Process {proc.pid} didn't terminate, forcing kill")
314
- proc.kill()
315
- self._active_processes.remove(proc)
316
- except Exception as e:
317
- logger.error(f'Error during process cleanup: {e}')
182
+ except Exception:
183
+ # If session creation fails, clean up the exit stack
184
+ if self._exit_stack:
185
+ await self._exit_stack.aclose()
186
+ self._exit_stack = None
187
+ raise
318
188
 
319
- # Clean up any tracked file handles
320
- for handle in list(self._active_file_handles):
189
+ async def close(self):
190
+ """Closes the session and cleans up resources."""
191
+ if self._exit_stack:
321
192
  try:
322
- if not handle.closed:
323
- logger.info('Closing file handle')
324
- handle.close()
325
- self._active_file_handles.remove(handle)
193
+ await self._exit_stack.aclose()
326
194
  except Exception as e:
327
- logger.error(f'Error closing file handle: {e}')
195
+ # Log the error but don't re-raise to avoid blocking shutdown
196
+ print(
197
+ f'Warning: Error during MCP session cleanup: {e}', file=self._errlog
198
+ )
199
+ finally:
200
+ self._exit_stack = None
201
+ self._session = None
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+
16
+ import logging
15
17
  from typing import Optional
16
18
 
17
19
  from google.genai.types import FunctionDeclaration
@@ -23,7 +25,6 @@ from .mcp_session_manager import retry_on_closed_resource
23
25
  # Attempt to import MCP Tool from the MCP library, and hints user to upgrade
24
26
  # their Python version to 3.10 if it fails.
25
27
  try:
26
- from mcp import ClientSession
27
28
  from mcp.types import Tool as McpBaseTool
28
29
  except ImportError as e:
29
30
  import sys
@@ -43,6 +44,8 @@ from ..base_tool import BaseTool
43
44
  from ..openapi_tool.openapi_spec_parser.rest_api_tool import to_gemini_schema
44
45
  from ..tool_context import ToolContext
45
46
 
47
+ logger = logging.getLogger("google_adk." + __name__)
48
+
46
49
 
47
50
  class MCPTool(BaseTool):
48
51
  """Turns a MCP Tool into a Vertex Agent Framework Tool.
@@ -53,44 +56,40 @@ class MCPTool(BaseTool):
53
56
 
54
57
  def __init__(
55
58
  self,
59
+ *,
56
60
  mcp_tool: McpBaseTool,
57
- mcp_session: ClientSession,
58
61
  mcp_session_manager: MCPSessionManager,
59
62
  auth_scheme: Optional[AuthScheme] = None,
60
- auth_credential: Optional[AuthCredential] | None = None,
63
+ auth_credential: Optional[AuthCredential] = None,
61
64
  ):
62
65
  """Initializes a MCPTool.
63
66
 
64
- This tool wraps a MCP Tool interface and an active MCP Session. It invokes
65
- the MCP Tool through executing the tool from remote MCP Session.
66
-
67
- Example:
68
- tool = MCPTool(mcp_tool=mcp_tool, mcp_session=mcp_session)
67
+ This tool wraps a MCP Tool interface and uses a session manager to
68
+ communicate with the MCP server.
69
69
 
70
70
  Args:
71
71
  mcp_tool: The MCP tool to wrap.
72
- mcp_session: The MCP session to use to call the tool.
72
+ mcp_session_manager: The MCP session manager to use for communication.
73
73
  auth_scheme: The authentication scheme to use.
74
74
  auth_credential: The authentication credential to use.
75
75
 
76
76
  Raises:
77
- ValueError: If mcp_tool or mcp_session is None.
77
+ ValueError: If mcp_tool or mcp_session_manager is None.
78
78
  """
79
79
  if mcp_tool is None:
80
80
  raise ValueError("mcp_tool cannot be None")
81
- if mcp_session is None:
82
- raise ValueError("mcp_session cannot be None")
83
- super().__init__(name=mcp_tool.name, description=mcp_tool.description or "")
81
+ if mcp_session_manager is None:
82
+ raise ValueError("mcp_session_manager cannot be None")
83
+ super().__init__(
84
+ name=mcp_tool.name,
85
+ description=mcp_tool.description if mcp_tool.description else "",
86
+ )
84
87
  self._mcp_tool = mcp_tool
85
- self._mcp_session = mcp_session
86
88
  self._mcp_session_manager = mcp_session_manager
87
89
  # TODO(cheliu): Support passing auth to MCP Server.
88
90
  self._auth_scheme = auth_scheme
89
91
  self._auth_credential = auth_credential
90
92
 
91
- async def _reinitialize_session(self):
92
- self._mcp_session = await self._mcp_session_manager.create_session()
93
-
94
93
  @override
95
94
  def _get_declaration(self) -> FunctionDeclaration:
96
95
  """Gets the function declaration for the tool.
@@ -105,7 +104,6 @@ class MCPTool(BaseTool):
105
104
  )
106
105
  return function_decl
107
106
 
108
- @override
109
107
  @retry_on_closed_resource("_reinitialize_session")
110
108
  async def run_async(self, *, args, tool_context: ToolContext):
111
109
  """Runs the tool asynchronously.
@@ -117,10 +115,15 @@ class MCPTool(BaseTool):
117
115
  Returns:
118
116
  Any: The response from the tool.
119
117
  """
118
+ # Get the session from the session manager
119
+ session = await self._mcp_session_manager.create_session()
120
+
120
121
  # TODO(cheliu): Support passing tool context to MCP Server.
121
- try:
122
- response = await self._mcp_session.call_tool(self.name, arguments=args)
123
- return response
124
- except Exception as e:
125
- print(e)
126
- raise e
122
+ response = await session.call_tool(self.name, arguments=args)
123
+ return response
124
+
125
+ async def _reinitialize_session(self):
126
+ """Reinitializes the session when connection is lost."""
127
+ # Close the old session and create a new one
128
+ await self._mcp_session_manager.close()
129
+ await self._mcp_session_manager.create_session()