google-adk 0.5.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 (139) hide show
  1. google/adk/agents/base_agent.py +76 -30
  2. google/adk/agents/callback_context.py +2 -6
  3. google/adk/agents/llm_agent.py +122 -30
  4. google/adk/agents/loop_agent.py +1 -1
  5. google/adk/agents/parallel_agent.py +7 -0
  6. google/adk/agents/readonly_context.py +8 -0
  7. google/adk/agents/run_config.py +1 -1
  8. google/adk/agents/sequential_agent.py +31 -0
  9. google/adk/agents/transcription_entry.py +4 -2
  10. google/adk/artifacts/gcs_artifact_service.py +1 -1
  11. google/adk/artifacts/in_memory_artifact_service.py +1 -1
  12. google/adk/auth/auth_credential.py +10 -2
  13. google/adk/auth/auth_preprocessor.py +7 -1
  14. google/adk/auth/auth_tool.py +3 -4
  15. google/adk/cli/agent_graph.py +5 -5
  16. google/adk/cli/browser/index.html +4 -4
  17. google/adk/cli/browser/{main-ULN5R5I5.js → main-PKDNKWJE.js} +59 -60
  18. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  19. google/adk/cli/cli.py +10 -9
  20. google/adk/cli/cli_deploy.py +7 -2
  21. google/adk/cli/cli_eval.py +109 -115
  22. google/adk/cli/cli_tools_click.py +179 -67
  23. google/adk/cli/fast_api.py +248 -197
  24. google/adk/cli/utils/agent_loader.py +137 -0
  25. google/adk/cli/utils/cleanup.py +40 -0
  26. google/adk/cli/utils/common.py +23 -0
  27. google/adk/cli/utils/evals.py +83 -0
  28. google/adk/cli/utils/logs.py +8 -5
  29. google/adk/code_executors/__init__.py +3 -1
  30. google/adk/code_executors/built_in_code_executor.py +52 -0
  31. google/adk/code_executors/code_execution_utils.py +2 -1
  32. google/adk/code_executors/container_code_executor.py +0 -1
  33. google/adk/code_executors/vertex_ai_code_executor.py +6 -8
  34. google/adk/evaluation/__init__.py +1 -1
  35. google/adk/evaluation/agent_evaluator.py +168 -128
  36. google/adk/evaluation/eval_case.py +104 -0
  37. google/adk/evaluation/eval_metrics.py +74 -0
  38. google/adk/evaluation/eval_result.py +86 -0
  39. google/adk/evaluation/eval_set.py +39 -0
  40. google/adk/evaluation/eval_set_results_manager.py +47 -0
  41. google/adk/evaluation/eval_sets_manager.py +43 -0
  42. google/adk/evaluation/evaluation_generator.py +88 -113
  43. google/adk/evaluation/evaluator.py +58 -0
  44. google/adk/evaluation/local_eval_set_results_manager.py +113 -0
  45. google/adk/evaluation/local_eval_sets_manager.py +264 -0
  46. google/adk/evaluation/response_evaluator.py +106 -1
  47. google/adk/evaluation/trajectory_evaluator.py +84 -2
  48. google/adk/events/event.py +6 -1
  49. google/adk/events/event_actions.py +6 -1
  50. google/adk/examples/base_example_provider.py +1 -0
  51. google/adk/examples/example_util.py +3 -2
  52. google/adk/flows/llm_flows/_code_execution.py +9 -1
  53. google/adk/flows/llm_flows/audio_transcriber.py +4 -3
  54. google/adk/flows/llm_flows/base_llm_flow.py +58 -21
  55. google/adk/flows/llm_flows/contents.py +3 -1
  56. google/adk/flows/llm_flows/functions.py +9 -8
  57. google/adk/flows/llm_flows/instructions.py +18 -80
  58. google/adk/flows/llm_flows/single_flow.py +2 -2
  59. google/adk/memory/__init__.py +1 -1
  60. google/adk/memory/_utils.py +23 -0
  61. google/adk/memory/base_memory_service.py +23 -21
  62. google/adk/memory/in_memory_memory_service.py +57 -25
  63. google/adk/memory/memory_entry.py +37 -0
  64. google/adk/memory/vertex_ai_rag_memory_service.py +38 -15
  65. google/adk/models/anthropic_llm.py +16 -9
  66. google/adk/models/base_llm.py +2 -1
  67. google/adk/models/base_llm_connection.py +2 -0
  68. google/adk/models/gemini_llm_connection.py +11 -11
  69. google/adk/models/google_llm.py +12 -2
  70. google/adk/models/lite_llm.py +80 -23
  71. google/adk/models/llm_response.py +16 -3
  72. google/adk/models/registry.py +1 -1
  73. google/adk/runners.py +98 -42
  74. google/adk/sessions/__init__.py +1 -1
  75. google/adk/sessions/_session_util.py +2 -1
  76. google/adk/sessions/base_session_service.py +6 -33
  77. google/adk/sessions/database_session_service.py +57 -67
  78. google/adk/sessions/in_memory_session_service.py +106 -24
  79. google/adk/sessions/session.py +3 -0
  80. google/adk/sessions/vertex_ai_session_service.py +44 -51
  81. google/adk/telemetry.py +7 -2
  82. google/adk/tools/__init__.py +4 -7
  83. google/adk/tools/_memory_entry_utils.py +30 -0
  84. google/adk/tools/agent_tool.py +10 -10
  85. google/adk/tools/apihub_tool/apihub_toolset.py +55 -74
  86. google/adk/tools/apihub_tool/clients/apihub_client.py +10 -3
  87. google/adk/tools/apihub_tool/clients/secret_client.py +1 -0
  88. google/adk/tools/application_integration_tool/application_integration_toolset.py +111 -85
  89. google/adk/tools/application_integration_tool/clients/connections_client.py +28 -1
  90. google/adk/tools/application_integration_tool/clients/integration_client.py +7 -5
  91. google/adk/tools/application_integration_tool/integration_connector_tool.py +69 -26
  92. google/adk/tools/base_toolset.py +96 -0
  93. google/adk/tools/bigquery/__init__.py +28 -0
  94. google/adk/tools/bigquery/bigquery_credentials.py +216 -0
  95. google/adk/tools/bigquery/bigquery_tool.py +116 -0
  96. google/adk/tools/{built_in_code_execution_tool.py → enterprise_search_tool.py} +17 -11
  97. google/adk/tools/function_parameter_parse_util.py +9 -2
  98. google/adk/tools/function_tool.py +33 -3
  99. google/adk/tools/get_user_choice_tool.py +1 -0
  100. google/adk/tools/google_api_tool/__init__.py +24 -70
  101. google/adk/tools/google_api_tool/google_api_tool.py +12 -6
  102. google/adk/tools/google_api_tool/{google_api_tool_set.py → google_api_toolset.py} +57 -55
  103. google/adk/tools/google_api_tool/google_api_toolsets.py +108 -0
  104. google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +40 -42
  105. google/adk/tools/google_search_tool.py +2 -2
  106. google/adk/tools/langchain_tool.py +96 -49
  107. google/adk/tools/load_memory_tool.py +14 -5
  108. google/adk/tools/mcp_tool/__init__.py +3 -2
  109. google/adk/tools/mcp_tool/conversion_utils.py +6 -2
  110. google/adk/tools/mcp_tool/mcp_session_manager.py +80 -69
  111. google/adk/tools/mcp_tool/mcp_tool.py +35 -32
  112. google/adk/tools/mcp_tool/mcp_toolset.py +99 -194
  113. google/adk/tools/openapi_tool/auth/credential_exchangers/base_credential_exchanger.py +1 -3
  114. google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py +6 -7
  115. google/adk/tools/openapi_tool/common/common.py +5 -1
  116. google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +7 -2
  117. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +27 -7
  118. google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +36 -32
  119. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +11 -1
  120. google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +1 -1
  121. google/adk/tools/preload_memory_tool.py +27 -18
  122. google/adk/tools/retrieval/__init__.py +1 -1
  123. google/adk/tools/retrieval/vertex_ai_rag_retrieval.py +1 -1
  124. google/adk/tools/toolbox_toolset.py +107 -0
  125. google/adk/tools/transfer_to_agent_tool.py +0 -1
  126. google/adk/utils/__init__.py +13 -0
  127. google/adk/utils/instructions_utils.py +131 -0
  128. google/adk/version.py +1 -1
  129. {google_adk-0.5.0.dist-info → google_adk-1.1.0.dist-info}/METADATA +18 -19
  130. google_adk-1.1.0.dist-info/RECORD +200 -0
  131. google/adk/agents/remote_agent.py +0 -50
  132. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -18
  133. google/adk/cli/fast_api.py.orig +0 -728
  134. google/adk/tools/google_api_tool/google_api_tool_sets.py +0 -112
  135. google/adk/tools/toolbox_tool.py +0 -46
  136. google_adk-0.5.0.dist-info/RECORD +0 -180
  137. {google_adk-0.5.0.dist-info → google_adk-1.1.0.dist-info}/WHEEL +0 -0
  138. {google_adk-0.5.0.dist-info → google_adk-1.1.0.dist-info}/entry_points.txt +0 -0
  139. {google_adk-0.5.0.dist-info → google_adk-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -25,10 +25,12 @@ from typing import TYPE_CHECKING
25
25
 
26
26
  from websockets.exceptions import ConnectionClosedOK
27
27
 
28
+ from . import functions
28
29
  from ...agents.base_agent import BaseAgent
29
30
  from ...agents.callback_context import CallbackContext
30
31
  from ...agents.invocation_context import InvocationContext
31
32
  from ...agents.live_request_queue import LiveRequestQueue
33
+ from ...agents.readonly_context import ReadonlyContext
32
34
  from ...agents.run_config import StreamingMode
33
35
  from ...agents.transcription_entry import TranscriptionEntry
34
36
  from ...events.event import Event
@@ -39,7 +41,6 @@ from ...telemetry import trace_call_llm
39
41
  from ...telemetry import trace_send_data
40
42
  from ...telemetry import tracer
41
43
  from ...tools.tool_context import ToolContext
42
- from . import functions
43
44
 
44
45
  if TYPE_CHECKING:
45
46
  from ...agents.llm_agent import LlmAgent
@@ -47,7 +48,7 @@ if TYPE_CHECKING:
47
48
  from ._base_llm_processor import BaseLlmRequestProcessor
48
49
  from ._base_llm_processor import BaseLlmResponseProcessor
49
50
 
50
- logger = logging.getLogger(__name__)
51
+ logger = logging.getLogger('google_adk.' + __name__)
51
52
 
52
53
 
53
54
  class BaseLlmFlow(ABC):
@@ -88,7 +89,12 @@ class BaseLlmFlow(ABC):
88
89
  if invocation_context.transcription_cache:
89
90
  from . import audio_transcriber
90
91
 
91
- audio_transcriber = audio_transcriber.AudioTranscriber()
92
+ audio_transcriber = audio_transcriber.AudioTranscriber(
93
+ init_client=True
94
+ if invocation_context.run_config.input_audio_transcription
95
+ is None
96
+ else False
97
+ )
92
98
  contents = audio_transcriber.transcribe_file(invocation_context)
93
99
  logger.debug('Sending history to model: %s', contents)
94
100
  await llm_connection.send_history(contents)
@@ -129,6 +135,18 @@ class BaseLlmFlow(ABC):
129
135
  # cancel the tasks that belongs to the closed connection.
130
136
  send_task.cancel()
131
137
  await llm_connection.close()
138
+ if (
139
+ event.content
140
+ and event.content.parts
141
+ and event.content.parts[0].function_response
142
+ and event.content.parts[0].function_response.name
143
+ == 'task_completed'
144
+ ):
145
+ # this is used for sequential agent to signal the end of the agent.
146
+ await asyncio.sleep(1)
147
+ # cancel the tasks that belongs to the closed connection.
148
+ send_task.cancel()
149
+ return
132
150
  finally:
133
151
  # Clean up
134
152
  if not send_task.done():
@@ -176,9 +194,12 @@ class BaseLlmFlow(ABC):
176
194
  # Cache audio data here for transcription
177
195
  if not invocation_context.transcription_cache:
178
196
  invocation_context.transcription_cache = []
179
- invocation_context.transcription_cache.append(
180
- TranscriptionEntry(role='user', data=live_request.blob)
181
- )
197
+ if not invocation_context.run_config.input_audio_transcription:
198
+ # if the live model's input transcription is not enabled, then
199
+ # we use our onwn audio transcriber to achieve that.
200
+ invocation_context.transcription_cache.append(
201
+ TranscriptionEntry(role='user', data=live_request.blob)
202
+ )
182
203
  await llm_connection.send_realtime(live_request.blob)
183
204
  if live_request.content:
184
205
  await llm_connection.send_content(live_request.content)
@@ -191,14 +212,22 @@ class BaseLlmFlow(ABC):
191
212
  llm_request: LlmRequest,
192
213
  ) -> AsyncGenerator[Event, None]:
193
214
  """Receive data from model and process events using BaseLlmConnection."""
194
- def get_author(llm_response):
215
+
216
+ def get_author_for_event(llm_response):
195
217
  """Get the author of the event.
196
218
 
197
219
  When the model returns transcription, the author is "user". Otherwise, the
198
- author is the agent.
220
+ author is the agent name(not 'model').
221
+
222
+ Args:
223
+ llm_response: The LLM response from the LLM call.
199
224
  """
200
- if llm_response and llm_response.content and llm_response.content.role == "user":
201
- return "user"
225
+ if (
226
+ llm_response
227
+ and llm_response.content
228
+ and llm_response.content.role == 'user'
229
+ ):
230
+ return 'user'
202
231
  else:
203
232
  return invocation_context.agent.name
204
233
 
@@ -209,7 +238,7 @@ class BaseLlmFlow(ABC):
209
238
  model_response_event = Event(
210
239
  id=Event.new_id(),
211
240
  invocation_id=invocation_context.invocation_id,
212
- author=get_author(llm_response),
241
+ author=get_author_for_event(llm_response),
213
242
  )
214
243
  async for event in self._postprocess_live(
215
244
  invocation_context,
@@ -220,13 +249,20 @@ class BaseLlmFlow(ABC):
220
249
  if (
221
250
  event.content
222
251
  and event.content.parts
223
- and event.content.parts[0].text
252
+ and event.content.parts[0].inline_data is None
224
253
  and not event.partial
225
254
  ):
255
+ # This can be either user data or transcription data.
256
+ # when output transcription enabled, it will contain model's
257
+ # transcription.
258
+ # when input transcription enabled, it will contain user
259
+ # transcription.
226
260
  if not invocation_context.transcription_cache:
227
261
  invocation_context.transcription_cache = []
228
262
  invocation_context.transcription_cache.append(
229
- TranscriptionEntry(role='model', data=event.content)
263
+ TranscriptionEntry(
264
+ role=event.content.role, data=event.content
265
+ )
230
266
  )
231
267
  yield event
232
268
  # Give opportunity for other tasks to run.
@@ -261,6 +297,7 @@ class BaseLlmFlow(ABC):
261
297
 
262
298
  # Calls the LLM.
263
299
  model_response_event = Event(
300
+ id=Event.new_id(),
264
301
  invocation_id=invocation_context.invocation_id,
265
302
  author=invocation_context.agent.name,
266
303
  branch=invocation_context.branch,
@@ -272,8 +309,8 @@ class BaseLlmFlow(ABC):
272
309
  async for event in self._postprocess_async(
273
310
  invocation_context, llm_request, llm_response, model_response_event
274
311
  ):
275
- # Use a new id for every event.
276
- event.id = Event.new_id()
312
+ # Update the mutable event id to avoid conflict
313
+ model_response_event.id = Event.new_id()
277
314
  yield event
278
315
 
279
316
  async def _preprocess_async(
@@ -291,7 +328,9 @@ class BaseLlmFlow(ABC):
291
328
  yield event
292
329
 
293
330
  # Run processors for tools.
294
- for tool in agent.canonical_tools:
331
+ for tool in await agent.canonical_tools(
332
+ ReadonlyContext(invocation_context)
333
+ ):
295
334
  tool_context = ToolContext(invocation_context)
296
335
  await tool.process_llm_request(
297
336
  tool_context=tool_context, llm_request=llm_request
@@ -433,14 +472,12 @@ class BaseLlmFlow(ABC):
433
472
  yield event
434
473
 
435
474
  def _get_agent_to_run(
436
- self, invocation_context: InvocationContext, transfer_to_agent
475
+ self, invocation_context: InvocationContext, agent_name: str
437
476
  ) -> BaseAgent:
438
477
  root_agent = invocation_context.agent.root_agent
439
- agent_to_run = root_agent.find_agent(transfer_to_agent)
478
+ agent_to_run = root_agent.find_agent(agent_name)
440
479
  if not agent_to_run:
441
- raise ValueError(
442
- f'Agent {transfer_to_agent} not found in the agent tree.'
443
- )
480
+ raise ValueError(f'Agent {agent_name} not found in the agent tree.')
444
481
  return agent_to_run
445
482
 
446
483
  async def _call_llm_async(
@@ -15,7 +15,9 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import copy
18
- from typing import AsyncGenerator, Generator, Optional
18
+ from typing import AsyncGenerator
19
+ from typing import Generator
20
+ from typing import Optional
19
21
 
20
22
  from google.genai import types
21
23
  from typing_extensions import override
@@ -41,7 +41,7 @@ from ...tools.tool_context import ToolContext
41
41
  AF_FUNCTION_CALL_ID_PREFIX = 'adk-'
42
42
  REQUEST_EUC_FUNCTION_CALL_NAME = 'adk_request_credential'
43
43
 
44
- logger = logging.getLogger(__name__)
44
+ logger = logging.getLogger('google_adk.' + __name__)
45
45
 
46
46
 
47
47
  def generate_client_function_call_id() -> str:
@@ -106,7 +106,7 @@ def generate_auth_event(
106
106
  args=AuthToolArguments(
107
107
  function_call_id=function_call_id,
108
108
  auth_config=auth_config,
109
- ).model_dump(exclude_none=True),
109
+ ).model_dump(exclude_none=True, by_alias=True),
110
110
  )
111
111
  request_euc_function_call.id = generate_client_function_call_id()
112
112
  long_running_tool_ids.add(request_euc_function_call.id)
@@ -153,22 +153,22 @@ async def handle_function_calls_async(
153
153
  function_args = function_call.args or {}
154
154
  function_response: Optional[dict] = None
155
155
 
156
- # before_tool_callback (sync or async)
157
- if agent.before_tool_callback:
158
- function_response = agent.before_tool_callback(
156
+ for callback in agent.canonical_before_tool_callbacks:
157
+ function_response = callback(
159
158
  tool=tool, args=function_args, tool_context=tool_context
160
159
  )
161
160
  if inspect.isawaitable(function_response):
162
161
  function_response = await function_response
162
+ if function_response:
163
+ break
163
164
 
164
165
  if not function_response:
165
166
  function_response = await __call_tool_async(
166
167
  tool, args=function_args, tool_context=tool_context
167
168
  )
168
169
 
169
- # after_tool_callback (sync or async)
170
- if agent.after_tool_callback:
171
- altered_function_response = agent.after_tool_callback(
170
+ for callback in agent.canonical_after_tool_callbacks:
171
+ altered_function_response = callback(
172
172
  tool=tool,
173
173
  args=function_args,
174
174
  tool_context=tool_context,
@@ -178,6 +178,7 @@ async def handle_function_calls_async(
178
178
  altered_function_response = await altered_function_response
179
179
  if altered_function_response is not None:
180
180
  function_response = altered_function_response
181
+ break
181
182
 
182
183
  if tool.is_long_running:
183
184
  # Allow long running function to return None to not provide function response.
@@ -26,6 +26,7 @@ from typing_extensions import override
26
26
  from ...agents.readonly_context import ReadonlyContext
27
27
  from ...events.event import Event
28
28
  from ...sessions.state import State
29
+ from ...utils import instructions_utils
29
30
  from ._base_llm_processor import BaseLlmRequestProcessor
30
31
 
31
32
  if TYPE_CHECKING:
@@ -53,16 +54,28 @@ class _InstructionsLlmRequestProcessor(BaseLlmRequestProcessor):
53
54
  if (
54
55
  isinstance(root_agent, LlmAgent) and root_agent.global_instruction
55
56
  ): # not empty str
56
- raw_si = root_agent.canonical_global_instruction(
57
- ReadonlyContext(invocation_context)
57
+ raw_si, bypass_state_injection = (
58
+ await root_agent.canonical_global_instruction(
59
+ ReadonlyContext(invocation_context)
60
+ )
58
61
  )
59
- si = await _populate_values(raw_si, invocation_context)
62
+ si = raw_si
63
+ if not bypass_state_injection:
64
+ si = await instructions_utils.inject_session_state(
65
+ raw_si, ReadonlyContext(invocation_context)
66
+ )
60
67
  llm_request.append_instructions([si])
61
68
 
62
69
  # Appends agent instructions if set.
63
70
  if agent.instruction: # not empty str
64
- raw_si = agent.canonical_instruction(ReadonlyContext(invocation_context))
65
- si = await _populate_values(raw_si, invocation_context)
71
+ raw_si, bypass_state_injection = await agent.canonical_instruction(
72
+ ReadonlyContext(invocation_context)
73
+ )
74
+ si = raw_si
75
+ if not bypass_state_injection:
76
+ si = await instructions_utils.inject_session_state(
77
+ raw_si, ReadonlyContext(invocation_context)
78
+ )
66
79
  llm_request.append_instructions([si])
67
80
 
68
81
  # Maintain async generator behavior
@@ -71,78 +84,3 @@ class _InstructionsLlmRequestProcessor(BaseLlmRequestProcessor):
71
84
 
72
85
 
73
86
  request_processor = _InstructionsLlmRequestProcessor()
74
-
75
-
76
- async def _populate_values(
77
- instruction_template: str,
78
- context: InvocationContext,
79
- ) -> str:
80
- """Populates values in the instruction template, e.g. state, artifact, etc."""
81
-
82
- async def _async_sub(pattern, repl_async_fn, string) -> str:
83
- result = []
84
- last_end = 0
85
- for match in re.finditer(pattern, string):
86
- result.append(string[last_end : match.start()])
87
- replacement = await repl_async_fn(match)
88
- result.append(replacement)
89
- last_end = match.end()
90
- result.append(string[last_end:])
91
- return ''.join(result)
92
-
93
- async def _replace_match(match) -> str:
94
- var_name = match.group().lstrip('{').rstrip('}').strip()
95
- optional = False
96
- if var_name.endswith('?'):
97
- optional = True
98
- var_name = var_name.removesuffix('?')
99
- if var_name.startswith('artifact.'):
100
- var_name = var_name.removeprefix('artifact.')
101
- if context.artifact_service is None:
102
- raise ValueError('Artifact service is not initialized.')
103
- artifact = await context.artifact_service.load_artifact(
104
- app_name=context.session.app_name,
105
- user_id=context.session.user_id,
106
- session_id=context.session.id,
107
- filename=var_name,
108
- )
109
- if not var_name:
110
- raise KeyError(f'Artifact {var_name} not found.')
111
- return str(artifact)
112
- else:
113
- if not _is_valid_state_name(var_name):
114
- return match.group()
115
- if var_name in context.session.state:
116
- return str(context.session.state[var_name])
117
- else:
118
- if optional:
119
- return ''
120
- else:
121
- raise KeyError(f'Context variable not found: `{var_name}`.')
122
-
123
- return await _async_sub(r'{+[^{}]*}+', _replace_match, instruction_template)
124
-
125
-
126
- def _is_valid_state_name(var_name):
127
- """Checks if the variable name is a valid state name.
128
-
129
- Valid state is either:
130
- - Valid identifier
131
- - <Valid prefix>:<Valid identifier>
132
- All the others will just return as it is.
133
-
134
- Args:
135
- var_name: The variable name to check.
136
-
137
- Returns:
138
- True if the variable name is a valid state name, False otherwise.
139
- """
140
- parts = var_name.split(':')
141
- if len(parts) == 1:
142
- return var_name.isidentifier()
143
-
144
- if len(parts) == 2:
145
- prefixes = [State.APP_PREFIX, State.USER_PREFIX, State.TEMP_PREFIX]
146
- if (parts[0] + ':') in prefixes:
147
- return parts[1].isidentifier()
148
- return False
@@ -16,16 +16,16 @@
16
16
 
17
17
  import logging
18
18
 
19
- from ...auth import auth_preprocessor
20
19
  from . import _code_execution
21
20
  from . import _nl_planning
22
21
  from . import basic
23
22
  from . import contents
24
23
  from . import identity
25
24
  from . import instructions
25
+ from ...auth import auth_preprocessor
26
26
  from .base_llm_flow import BaseLlmFlow
27
27
 
28
- logger = logging.getLogger(__name__)
28
+ logger = logging.getLogger('google_adk.' + __name__)
29
29
 
30
30
 
31
31
  class SingleFlow(BaseLlmFlow):
@@ -16,7 +16,7 @@ import logging
16
16
  from .base_memory_service import BaseMemoryService
17
17
  from .in_memory_memory_service import InMemoryMemoryService
18
18
 
19
- logger = logging.getLogger(__name__)
19
+ logger = logging.getLogger('google_adk.' + __name__)
20
20
 
21
21
  __all__ = [
22
22
  'BaseMemoryService',
@@ -0,0 +1,23 @@
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
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import datetime
19
+
20
+
21
+ def format_timestamp(timestamp: float) -> str:
22
+ """Formats the timestamp of the memory entry."""
23
+ return datetime.fromtimestamp(timestamp).isoformat()
@@ -12,46 +12,44 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- import abc
16
15
 
17
- from pydantic import BaseModel
18
- from pydantic import Field
16
+ from __future__ import annotations
19
17
 
20
- from ..events.event import Event
21
- from ..sessions.session import Session
18
+ from abc import ABC
19
+ from abc import abstractmethod
20
+ from typing import TYPE_CHECKING
22
21
 
22
+ from pydantic import BaseModel
23
+ from pydantic import Field
23
24
 
24
- class MemoryResult(BaseModel):
25
- """Represents a single memory retrieval result.
26
-
27
- Attributes:
28
- session_id: The session id associated with the memory.
29
- events: A list of events in the session.
30
- """
25
+ from .memory_entry import MemoryEntry
31
26
 
32
- session_id: str
33
- events: list[Event]
27
+ if TYPE_CHECKING:
28
+ from ..sessions.session import Session
34
29
 
35
30
 
36
31
  class SearchMemoryResponse(BaseModel):
37
32
  """Represents the response from a memory search.
38
33
 
39
34
  Attributes:
40
- memories: A list of memory results matching the search query.
35
+ memories: A list of memory entries that relate to the search query.
41
36
  """
42
37
 
43
- memories: list[MemoryResult] = Field(default_factory=list)
38
+ memories: list[MemoryEntry] = Field(default_factory=list)
44
39
 
45
40
 
46
- class BaseMemoryService(abc.ABC):
41
+ class BaseMemoryService(ABC):
47
42
  """Base class for memory services.
48
43
 
49
44
  The service provides functionalities to ingest sessions into memory so that
50
45
  the memory can be used for user queries.
51
46
  """
52
47
 
53
- @abc.abstractmethod
54
- async def add_session_to_memory(self, session: Session):
48
+ @abstractmethod
49
+ async def add_session_to_memory(
50
+ self,
51
+ session: Session,
52
+ ):
55
53
  """Adds a session to the memory service.
56
54
 
57
55
  A session may be added multiple times during its lifetime.
@@ -60,9 +58,13 @@ class BaseMemoryService(abc.ABC):
60
58
  session: The session to add.
61
59
  """
62
60
 
63
- @abc.abstractmethod
61
+ @abstractmethod
64
62
  async def search_memory(
65
- self, *, app_name: str, user_id: str, query: str
63
+ self,
64
+ *,
65
+ app_name: str,
66
+ user_id: str,
67
+ query: str,
66
68
  ) -> SearchMemoryResponse:
67
69
  """Searches for sessions that match the query.
68
70
 
@@ -12,11 +12,31 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from ..events.event import Event
16
- from ..sessions.session import Session
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import TYPE_CHECKING
20
+
21
+ from typing_extensions import override
22
+
23
+ from . import _utils
17
24
  from .base_memory_service import BaseMemoryService
18
- from .base_memory_service import MemoryResult
19
25
  from .base_memory_service import SearchMemoryResponse
26
+ from .memory_entry import MemoryEntry
27
+
28
+ if TYPE_CHECKING:
29
+ from ..events.event import Event
30
+ from ..sessions.session import Session
31
+
32
+
33
+ def _user_key(app_name: str, user_id: str):
34
+ return f'{app_name}/{user_id}'
35
+
36
+
37
+ def _extract_words_lower(text: str) -> set[str]:
38
+ """Extracts words from a string and converts them to lowercase."""
39
+ return set([word.lower() for word in re.findall(r'[A-Za-z]+', text)])
20
40
 
21
41
 
22
42
  class InMemoryMemoryService(BaseMemoryService):
@@ -26,37 +46,49 @@ class InMemoryMemoryService(BaseMemoryService):
26
46
  """
27
47
 
28
48
  def __init__(self):
29
- self.session_events: dict[str, list[Event]] = {}
30
- """keys are app_name/user_id/session_id"""
49
+ self._session_events: dict[str, dict[str, list[Event]]] = {}
50
+ """Keys are app_name/user_id, session_id. Values are session event lists."""
31
51
 
52
+ @override
32
53
  async def add_session_to_memory(self, session: Session):
33
- key = f'{session.app_name}/{session.user_id}/{session.id}'
34
- self.session_events[key] = [
35
- event for event in session.events if event.content
54
+ user_key = _user_key(session.app_name, session.user_id)
55
+ self._session_events[user_key] = self._session_events.get(
56
+ _user_key(session.app_name, session.user_id), {}
57
+ )
58
+ self._session_events[user_key][session.id] = [
59
+ event
60
+ for event in session.events
61
+ if event.content and event.content.parts
36
62
  ]
37
63
 
64
+ @override
38
65
  async def search_memory(
39
66
  self, *, app_name: str, user_id: str, query: str
40
67
  ) -> SearchMemoryResponse:
41
- """Prototyping purpose only."""
42
- keywords = set(query.lower().split())
68
+ user_key = _user_key(app_name, user_id)
69
+ if user_key not in self._session_events:
70
+ return SearchMemoryResponse()
71
+
72
+ words_in_query = set(query.lower().split())
43
73
  response = SearchMemoryResponse()
44
- for key, events in self.session_events.items():
45
- if not key.startswith(f'{app_name}/{user_id}/'):
46
- continue
47
- matched_events = []
48
- for event in events:
74
+
75
+ for session_events in self._session_events[user_key].values():
76
+ for event in session_events:
49
77
  if not event.content or not event.content.parts:
50
78
  continue
51
- parts = event.content.parts
52
- text = '\n'.join([part.text for part in parts if part.text]).lower()
53
- for keyword in keywords:
54
- if keyword in text:
55
- matched_events.append(event)
56
- break
57
- if matched_events:
58
- session_id = key.split('/')[-1]
59
- response.memories.append(
60
- MemoryResult(session_id=session_id, events=matched_events)
79
+ words_in_event = _extract_words_lower(
80
+ ' '.join([part.text for part in event.content.parts if part.text])
61
81
  )
82
+ if not words_in_event:
83
+ continue
84
+
85
+ if any(query_word in words_in_event for query_word in words_in_query):
86
+ response.memories.append(
87
+ MemoryEntry(
88
+ content=event.content,
89
+ author=event.author,
90
+ timestamp=_utils.format_timestamp(event.timestamp),
91
+ )
92
+ )
93
+
62
94
  return response
@@ -0,0 +1,37 @@
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
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Optional
19
+
20
+ from google.genai import types
21
+ from pydantic import BaseModel
22
+
23
+
24
+ class MemoryEntry(BaseModel):
25
+ """Represent one memory entry."""
26
+
27
+ content: types.Content
28
+ """The main content of the memory."""
29
+
30
+ author: Optional[str] = None
31
+ """The author of the memory."""
32
+
33
+ timestamp: Optional[str] = None
34
+ """The timestamp when the original content of this memory happened.
35
+
36
+ This string will be forwarded to LLM. Preferred format is ISO 8601 format.
37
+ """