google-adk 1.5.0__py3-none-any.whl → 1.6.1__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 (60) hide show
  1. google/adk/a2a/converters/event_converter.py +257 -36
  2. google/adk/a2a/converters/part_converter.py +93 -25
  3. google/adk/a2a/converters/request_converter.py +12 -32
  4. google/adk/a2a/converters/utils.py +22 -4
  5. google/adk/a2a/executor/__init__.py +13 -0
  6. google/adk/a2a/executor/a2a_agent_executor.py +260 -0
  7. google/adk/a2a/executor/task_result_aggregator.py +71 -0
  8. google/adk/a2a/logs/__init__.py +13 -0
  9. google/adk/a2a/logs/log_utils.py +349 -0
  10. google/adk/agents/base_agent.py +54 -0
  11. google/adk/agents/llm_agent.py +15 -0
  12. google/adk/agents/remote_a2a_agent.py +532 -0
  13. google/adk/artifacts/in_memory_artifact_service.py +6 -3
  14. google/adk/cli/browser/chunk-EQDQRRRY.js +1 -0
  15. google/adk/cli/browser/chunk-TXJFAAIW.js +2 -0
  16. google/adk/cli/browser/index.html +4 -3
  17. google/adk/cli/browser/main-RXDVX3K6.js +3914 -0
  18. google/adk/cli/browser/polyfills-FFHMD2TL.js +17 -0
  19. google/adk/cli/cli_deploy.py +4 -1
  20. google/adk/cli/cli_eval.py +8 -6
  21. google/adk/cli/cli_tools_click.py +30 -10
  22. google/adk/cli/fast_api.py +120 -5
  23. google/adk/cli/utils/agent_loader.py +12 -0
  24. google/adk/evaluation/agent_evaluator.py +107 -10
  25. google/adk/evaluation/base_eval_service.py +157 -0
  26. google/adk/evaluation/constants.py +20 -0
  27. google/adk/evaluation/eval_case.py +3 -3
  28. google/adk/evaluation/eval_metrics.py +39 -0
  29. google/adk/evaluation/evaluation_generator.py +1 -1
  30. google/adk/evaluation/final_response_match_v2.py +230 -0
  31. google/adk/evaluation/llm_as_judge.py +141 -0
  32. google/adk/evaluation/llm_as_judge_utils.py +48 -0
  33. google/adk/evaluation/metric_evaluator_registry.py +89 -0
  34. google/adk/evaluation/response_evaluator.py +38 -211
  35. google/adk/evaluation/safety_evaluator.py +54 -0
  36. google/adk/evaluation/trajectory_evaluator.py +16 -2
  37. google/adk/evaluation/vertex_ai_eval_facade.py +147 -0
  38. google/adk/events/event.py +2 -4
  39. google/adk/flows/llm_flows/base_llm_flow.py +2 -0
  40. google/adk/memory/in_memory_memory_service.py +3 -2
  41. google/adk/models/lite_llm.py +50 -10
  42. google/adk/runners.py +27 -10
  43. google/adk/sessions/database_session_service.py +25 -7
  44. google/adk/sessions/in_memory_session_service.py +5 -1
  45. google/adk/sessions/vertex_ai_session_service.py +67 -42
  46. google/adk/tools/bigquery/config.py +11 -1
  47. google/adk/tools/bigquery/query_tool.py +306 -12
  48. google/adk/tools/enterprise_search_tool.py +2 -2
  49. google/adk/tools/function_tool.py +7 -1
  50. google/adk/tools/google_search_tool.py +1 -1
  51. google/adk/tools/mcp_tool/mcp_session_manager.py +44 -30
  52. google/adk/tools/mcp_tool/mcp_tool.py +44 -7
  53. google/adk/version.py +1 -1
  54. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/METADATA +6 -4
  55. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/RECORD +58 -42
  56. google/adk/cli/browser/main-JAAWEV7F.js +0 -92
  57. google/adk/cli/browser/polyfills-B6TNHZQ6.js +0 -17
  58. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/WHEEL +0 -0
  59. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/entry_points.txt +0 -0
  60. {google_adk-1.5.0.dist-info → google_adk-1.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -31,42 +31,24 @@ except ImportError as e:
31
31
  from google.genai import types as genai_types
32
32
 
33
33
  from ...runners import RunConfig
34
- from ...utils.feature_decorator import working_in_progress
34
+ from ...utils.feature_decorator import experimental
35
35
  from .part_converter import convert_a2a_part_to_genai_part
36
- from .utils import _from_a2a_context_id
37
- from .utils import _get_adk_metadata_key
38
36
 
39
37
 
40
- def _get_user_id(request: RequestContext, user_id_from_context: str) -> str:
38
+ def _get_user_id(request: RequestContext) -> str:
41
39
  # Get user from call context if available (auth is enabled on a2a server)
42
- if request.call_context and request.call_context.user:
40
+ if (
41
+ request.call_context
42
+ and request.call_context.user
43
+ and request.call_context.user.user_name
44
+ ):
43
45
  return request.call_context.user.user_name
44
46
 
45
- # Get user from context id if available
46
- if user_id_from_context:
47
- return user_id_from_context
47
+ # Get user from context id
48
+ return f'A2A_USER_{request.context_id}'
48
49
 
49
- # Get user from message metadata if available (client is an ADK agent)
50
- if request.message.metadata:
51
- user_id = request.message.metadata.get(_get_adk_metadata_key('user_id'))
52
- if user_id:
53
- return f'ADK_USER_{user_id}'
54
50
 
55
- # Get user from task if available (client is a an ADK agent)
56
- if request.current_task:
57
- user_id = request.current_task.metadata.get(
58
- _get_adk_metadata_key('user_id')
59
- )
60
- if user_id:
61
- return f'ADK_USER_{user_id}'
62
- return (
63
- f'temp_user_{request.task_id}'
64
- if request.task_id
65
- else f'TEMP_USER_{request.message.messageId}'
66
- )
67
-
68
-
69
- @working_in_progress
51
+ @experimental
70
52
  def convert_a2a_request_to_adk_run_args(
71
53
  request: RequestContext,
72
54
  ) -> dict[str, Any]:
@@ -74,11 +56,9 @@ def convert_a2a_request_to_adk_run_args(
74
56
  if not request.message:
75
57
  raise ValueError('Request message cannot be None')
76
58
 
77
- _, user_id, session_id = _from_a2a_context_id(request.context_id)
78
-
79
59
  return {
80
- 'user_id': _get_user_id(request, user_id),
81
- 'session_id': session_id,
60
+ 'user_id': _get_user_id(request),
61
+ 'session_id': request.context_id,
82
62
  'new_message': genai_types.Content(
83
63
  role='user',
84
64
  parts=[
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  ADK_METADATA_KEY_PREFIX = "adk_"
18
18
  ADK_CONTEXT_ID_PREFIX = "ADK"
19
+ ADK_CONTEXT_ID_SEPARATOR = "/"
19
20
 
20
21
 
21
22
  def _get_adk_metadata_key(key: str) -> str:
@@ -45,8 +46,17 @@ def _to_a2a_context_id(app_name: str, user_id: str, session_id: str) -> str:
45
46
 
46
47
  Returns:
47
48
  The A2A context id.
49
+
50
+ Raises:
51
+ ValueError: If any of the input parameters are empty or None.
48
52
  """
49
- return [ADK_CONTEXT_ID_PREFIX, app_name, user_id, session_id].join("$")
53
+ if not all([app_name, user_id, session_id]):
54
+ raise ValueError(
55
+ "All parameters (app_name, user_id, session_id) must be non-empty"
56
+ )
57
+ return ADK_CONTEXT_ID_SEPARATOR.join(
58
+ [ADK_CONTEXT_ID_PREFIX, app_name, user_id, session_id]
59
+ )
50
60
 
51
61
 
52
62
  def _from_a2a_context_id(context_id: str) -> tuple[str, str, str]:
@@ -64,8 +74,16 @@ def _from_a2a_context_id(context_id: str) -> tuple[str, str, str]:
64
74
  if not context_id:
65
75
  return None, None, None
66
76
 
67
- prefix, app_name, user_id, session_id = context_id.split("$")
68
- if prefix == "ADK" and app_name and user_id and session_id:
69
- return app_name, user_id, session_id
77
+ try:
78
+ parts = context_id.split(ADK_CONTEXT_ID_SEPARATOR)
79
+ if len(parts) != 4:
80
+ return None, None, None
81
+
82
+ prefix, app_name, user_id, session_id = parts
83
+ if prefix == ADK_CONTEXT_ID_PREFIX and app_name and user_id and session_id:
84
+ return app_name, user_id, session_id
85
+ except ValueError:
86
+ # Handle any split errors gracefully
87
+ pass
70
88
 
71
89
  return None, None, None
@@ -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,260 @@
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
+ from __future__ import annotations
16
+
17
+ from datetime import datetime
18
+ from datetime import timezone
19
+ import inspect
20
+ import logging
21
+ from typing import Any
22
+ from typing import Awaitable
23
+ from typing import Callable
24
+ from typing import Optional
25
+ import uuid
26
+
27
+ try:
28
+ from a2a.server.agent_execution import AgentExecutor
29
+ from a2a.server.agent_execution.context import RequestContext
30
+ from a2a.server.events.event_queue import EventQueue
31
+ from a2a.types import Message
32
+ from a2a.types import Role
33
+ from a2a.types import TaskState
34
+ from a2a.types import TaskStatus
35
+ from a2a.types import TaskStatusUpdateEvent
36
+ from a2a.types import TextPart
37
+
38
+ except ImportError as e:
39
+ import sys
40
+
41
+ if sys.version_info < (3, 10):
42
+ raise ImportError(
43
+ 'A2A requires Python 3.10 or above. Please upgrade your Python version.'
44
+ ) from e
45
+ else:
46
+ raise e
47
+ from google.adk.runners import Runner
48
+ from pydantic import BaseModel
49
+ from typing_extensions import override
50
+
51
+ from ...utils.feature_decorator import experimental
52
+ from ..converters.event_converter import convert_event_to_a2a_events
53
+ from ..converters.request_converter import convert_a2a_request_to_adk_run_args
54
+ from ..converters.utils import _get_adk_metadata_key
55
+ from .task_result_aggregator import TaskResultAggregator
56
+
57
+ logger = logging.getLogger('google_adk.' + __name__)
58
+
59
+
60
+ @experimental
61
+ class A2aAgentExecutorConfig(BaseModel):
62
+ """Configuration for the A2aAgentExecutor."""
63
+
64
+ pass
65
+
66
+
67
+ @experimental
68
+ class A2aAgentExecutor(AgentExecutor):
69
+ """An AgentExecutor that runs an ADK Agent against an A2A request and
70
+ publishes updates to an event queue.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ *,
76
+ runner: Runner | Callable[..., Runner | Awaitable[Runner]],
77
+ config: Optional[A2aAgentExecutorConfig] = None,
78
+ ):
79
+ super().__init__()
80
+ self._runner = runner
81
+ self._config = config
82
+
83
+ async def _resolve_runner(self) -> Runner:
84
+ """Resolve the runner, handling cases where it's a callable that returns a Runner."""
85
+ # If already resolved and cached, return it
86
+ if isinstance(self._runner, Runner):
87
+ return self._runner
88
+ if callable(self._runner):
89
+ # Call the function to get the runner
90
+ result = self._runner()
91
+
92
+ # Handle async callables
93
+ if inspect.iscoroutine(result):
94
+ resolved_runner = await result
95
+ else:
96
+ resolved_runner = result
97
+
98
+ # Cache the resolved runner for future calls
99
+ self._runner = resolved_runner
100
+ return resolved_runner
101
+
102
+ raise TypeError(
103
+ 'Runner must be a Runner instance or a callable that returns a'
104
+ f' Runner, got {type(self._runner)}'
105
+ )
106
+
107
+ @override
108
+ async def cancel(self, context: RequestContext, event_queue: EventQueue):
109
+ """Cancel the execution."""
110
+ # TODO: Implement proper cancellation logic if needed
111
+ raise NotImplementedError('Cancellation is not supported')
112
+
113
+ @override
114
+ async def execute(
115
+ self,
116
+ context: RequestContext,
117
+ event_queue: EventQueue,
118
+ ):
119
+ """Executes an A2A request and publishes updates to the event queue
120
+ specified. It runs as following:
121
+ * Takes the input from the A2A request
122
+ * Convert the input to ADK input content, and runs the ADK agent
123
+ * Collects output events of the underlying ADK Agent
124
+ * Converts the ADK output events into A2A task updates
125
+ * Publishes the updates back to A2A server via event queue
126
+ """
127
+ if not context.message:
128
+ raise ValueError('A2A request must have a message')
129
+
130
+ # for new task, create a task submitted event
131
+ if not context.current_task:
132
+ await event_queue.enqueue_event(
133
+ TaskStatusUpdateEvent(
134
+ taskId=context.task_id,
135
+ status=TaskStatus(
136
+ state=TaskState.submitted,
137
+ message=context.message,
138
+ timestamp=datetime.now(timezone.utc).isoformat(),
139
+ ),
140
+ contextId=context.context_id,
141
+ final=False,
142
+ )
143
+ )
144
+
145
+ # Handle the request and publish updates to the event queue
146
+ try:
147
+ await self._handle_request(context, event_queue)
148
+ except Exception as e:
149
+ logger.error('Error handling A2A request: %s', e, exc_info=True)
150
+ # Publish failure event
151
+ try:
152
+ await event_queue.enqueue_event(
153
+ TaskStatusUpdateEvent(
154
+ taskId=context.task_id,
155
+ status=TaskStatus(
156
+ state=TaskState.failed,
157
+ timestamp=datetime.now(timezone.utc).isoformat(),
158
+ message=Message(
159
+ messageId=str(uuid.uuid4()),
160
+ role=Role.agent,
161
+ parts=[TextPart(text=str(e))],
162
+ ),
163
+ ),
164
+ contextId=context.context_id,
165
+ final=True,
166
+ )
167
+ )
168
+ except Exception as enqueue_error:
169
+ logger.error(
170
+ 'Failed to publish failure event: %s', enqueue_error, exc_info=True
171
+ )
172
+
173
+ async def _handle_request(
174
+ self,
175
+ context: RequestContext,
176
+ event_queue: EventQueue,
177
+ ):
178
+ # Resolve the runner instance
179
+ runner = await self._resolve_runner()
180
+
181
+ # Convert the a2a request to ADK run args
182
+ run_args = convert_a2a_request_to_adk_run_args(context)
183
+
184
+ # ensure the session exists
185
+ session = await self._prepare_session(context, run_args, runner)
186
+
187
+ # create invocation context
188
+ invocation_context = runner._new_invocation_context(
189
+ session=session,
190
+ new_message=run_args['new_message'],
191
+ run_config=run_args['run_config'],
192
+ )
193
+
194
+ # publish the task working event
195
+ await event_queue.enqueue_event(
196
+ TaskStatusUpdateEvent(
197
+ taskId=context.task_id,
198
+ status=TaskStatus(
199
+ state=TaskState.working,
200
+ timestamp=datetime.now(timezone.utc).isoformat(),
201
+ ),
202
+ contextId=context.context_id,
203
+ final=False,
204
+ metadata={
205
+ _get_adk_metadata_key('app_name'): runner.app_name,
206
+ _get_adk_metadata_key('user_id'): run_args['user_id'],
207
+ _get_adk_metadata_key('session_id'): run_args['session_id'],
208
+ },
209
+ )
210
+ )
211
+
212
+ task_result_aggregator = TaskResultAggregator()
213
+ async for adk_event in runner.run_async(**run_args):
214
+ for a2a_event in convert_event_to_a2a_events(
215
+ adk_event, invocation_context, context.task_id, context.context_id
216
+ ):
217
+ task_result_aggregator.process_event(a2a_event)
218
+ await event_queue.enqueue_event(a2a_event)
219
+
220
+ # publish the task result event - this is final
221
+ await event_queue.enqueue_event(
222
+ TaskStatusUpdateEvent(
223
+ taskId=context.task_id,
224
+ status=TaskStatus(
225
+ state=(
226
+ task_result_aggregator.task_state
227
+ if task_result_aggregator.task_state != TaskState.working
228
+ else TaskState.completed
229
+ ),
230
+ timestamp=datetime.now(timezone.utc).isoformat(),
231
+ message=task_result_aggregator.task_status_message,
232
+ ),
233
+ contextId=context.context_id,
234
+ final=True,
235
+ )
236
+ )
237
+
238
+ async def _prepare_session(
239
+ self, context: RequestContext, run_args: dict[str, Any], runner: Runner
240
+ ):
241
+
242
+ session_id = run_args['session_id']
243
+ # create a new session if not exists
244
+ user_id = run_args['user_id']
245
+ session = await runner.session_service.get_session(
246
+ app_name=runner.app_name,
247
+ user_id=user_id,
248
+ session_id=session_id,
249
+ )
250
+ if session is None:
251
+ session = await runner.session_service.create_session(
252
+ app_name=runner.app_name,
253
+ user_id=user_id,
254
+ state={},
255
+ session_id=session_id,
256
+ )
257
+ # Update run_args with the new session_id
258
+ run_args['session_id'] = session.id
259
+
260
+ return session
@@ -0,0 +1,71 @@
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
+ from __future__ import annotations
16
+
17
+ from a2a.server.events import Event
18
+ from a2a.types import Message
19
+ from a2a.types import TaskState
20
+ from a2a.types import TaskStatusUpdateEvent
21
+
22
+ from ...utils.feature_decorator import experimental
23
+
24
+
25
+ @experimental
26
+ class TaskResultAggregator:
27
+ """Aggregates the task status updates and provides the final task state."""
28
+
29
+ def __init__(self):
30
+ self._task_state = TaskState.working
31
+ self._task_status_message = None
32
+
33
+ def process_event(self, event: Event):
34
+ """Process an event from the agent run and detect signals about the task status.
35
+ Priority of task state:
36
+ - failed
37
+ - auth_required
38
+ - input_required
39
+ - working
40
+ """
41
+ if isinstance(event, TaskStatusUpdateEvent):
42
+ if event.status.state == TaskState.failed:
43
+ self._task_state = TaskState.failed
44
+ self._task_status_message = event.status.message
45
+ elif (
46
+ event.status.state == TaskState.auth_required
47
+ and self._task_state != TaskState.failed
48
+ ):
49
+ self._task_state = TaskState.auth_required
50
+ self._task_status_message = event.status.message
51
+ elif (
52
+ event.status.state == TaskState.input_required
53
+ and self._task_state
54
+ not in (TaskState.failed, TaskState.auth_required)
55
+ ):
56
+ self._task_state = TaskState.input_required
57
+ self._task_status_message = event.status.message
58
+ # final state is already recorded and make sure the intermediate state is
59
+ # always working because other state may terminate the event aggregation
60
+ # in a2a request handler
61
+ elif self._task_state == TaskState.working:
62
+ self._task_status_message = event.status.message
63
+ event.status.state = TaskState.working
64
+
65
+ @property
66
+ def task_state(self) -> TaskState:
67
+ return self._task_state
68
+
69
+ @property
70
+ def task_status_message(self) -> Message | None:
71
+ return self._task_status_message
@@ -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.