google-adk 0.3.0__py3-none-any.whl → 0.5.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 (71) hide show
  1. google/adk/agents/active_streaming_tool.py +1 -0
  2. google/adk/agents/base_agent.py +27 -29
  3. google/adk/agents/callback_context.py +4 -4
  4. google/adk/agents/invocation_context.py +1 -0
  5. google/adk/agents/langgraph_agent.py +1 -0
  6. google/adk/agents/live_request_queue.py +1 -0
  7. google/adk/agents/llm_agent.py +54 -14
  8. google/adk/agents/run_config.py +4 -0
  9. google/adk/agents/transcription_entry.py +1 -0
  10. google/adk/artifacts/base_artifact_service.py +5 -10
  11. google/adk/artifacts/gcs_artifact_service.py +8 -8
  12. google/adk/artifacts/in_memory_artifact_service.py +5 -5
  13. google/adk/auth/auth_credential.py +4 -5
  14. google/adk/cli/browser/index.html +1 -1
  15. google/adk/cli/browser/{main-HWIBUY2R.js → main-ULN5R5I5.js} +40 -39
  16. google/adk/cli/cli.py +54 -47
  17. google/adk/cli/cli_eval.py +13 -11
  18. google/adk/cli/cli_tools_click.py +58 -7
  19. google/adk/cli/fast_api.py +11 -11
  20. google/adk/cli/fast_api.py.orig +728 -0
  21. google/adk/evaluation/agent_evaluator.py +3 -3
  22. google/adk/evaluation/evaluation_constants.py +1 -0
  23. google/adk/evaluation/evaluation_generator.py +5 -5
  24. google/adk/evaluation/response_evaluator.py +1 -1
  25. google/adk/events/event.py +1 -0
  26. google/adk/events/event_actions.py +10 -4
  27. google/adk/examples/example.py +1 -0
  28. google/adk/flows/__init__.py +0 -1
  29. google/adk/flows/llm_flows/_code_execution.py +10 -10
  30. google/adk/flows/llm_flows/base_llm_flow.py +40 -15
  31. google/adk/flows/llm_flows/basic.py +3 -0
  32. google/adk/flows/llm_flows/contents.py +9 -5
  33. google/adk/flows/llm_flows/functions.py +38 -16
  34. google/adk/flows/llm_flows/instructions.py +17 -6
  35. google/adk/memory/base_memory_service.py +4 -2
  36. google/adk/memory/in_memory_memory_service.py +2 -2
  37. google/adk/memory/vertex_ai_rag_memory_service.py +2 -2
  38. google/adk/models/anthropic_llm.py +20 -2
  39. google/adk/models/base_llm.py +45 -4
  40. google/adk/models/gemini_llm_connection.py +14 -1
  41. google/adk/models/google_llm.py +0 -42
  42. google/adk/models/lite_llm.py +17 -17
  43. google/adk/models/llm_request.py +1 -1
  44. google/adk/models/llm_response.py +1 -1
  45. google/adk/runners.py +5 -5
  46. google/adk/sessions/_session_util.py +43 -0
  47. google/adk/sessions/base_session_service.py +3 -0
  48. google/adk/sessions/database_session_service.py +63 -46
  49. google/adk/sessions/in_memory_session_service.py +3 -3
  50. google/adk/sessions/session.py +1 -0
  51. google/adk/sessions/vertex_ai_session_service.py +7 -5
  52. google/adk/tools/agent_tool.py +7 -4
  53. google/adk/tools/application_integration_tool/__init__.py +2 -0
  54. google/adk/tools/application_integration_tool/application_integration_toolset.py +48 -26
  55. google/adk/tools/application_integration_tool/clients/connections_client.py +33 -77
  56. google/adk/tools/application_integration_tool/integration_connector_tool.py +159 -0
  57. google/adk/tools/function_tool.py +42 -0
  58. google/adk/tools/load_artifacts_tool.py +4 -4
  59. google/adk/tools/load_memory_tool.py +4 -2
  60. google/adk/tools/mcp_tool/conversion_utils.py +1 -1
  61. google/adk/tools/mcp_tool/mcp_session_manager.py +14 -0
  62. google/adk/tools/openapi_tool/common/common.py +2 -5
  63. google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +13 -3
  64. google/adk/tools/preload_memory_tool.py +1 -1
  65. google/adk/tools/tool_context.py +4 -4
  66. google/adk/version.py +1 -1
  67. {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/METADATA +3 -7
  68. {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/RECORD +71 -68
  69. {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/WHEEL +0 -0
  70. {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/entry_points.txt +0 -0
  71. {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -14,9 +14,9 @@
14
14
  from __future__ import annotations
15
15
 
16
16
  from abc import abstractmethod
17
- from typing import AsyncGenerator
18
- from typing import TYPE_CHECKING
17
+ from typing import AsyncGenerator, TYPE_CHECKING
19
18
 
19
+ from google.genai import types
20
20
  from pydantic import BaseModel
21
21
  from pydantic import ConfigDict
22
22
 
@@ -32,14 +32,13 @@ class BaseLlm(BaseModel):
32
32
 
33
33
  Attributes:
34
34
  model: The name of the LLM, e.g. gemini-1.5-flash or gemini-1.5-flash-001.
35
- model_config: The model config
36
35
  """
37
36
 
38
37
  model_config = ConfigDict(
39
38
  # This allows us to use arbitrary types in the model. E.g. PIL.Image.
40
39
  arbitrary_types_allowed=True,
41
40
  )
42
- """The model config."""
41
+ """The pydantic model config."""
43
42
 
44
43
  model: str
45
44
  """The name of the LLM, e.g. gemini-1.5-flash or gemini-1.5-flash-001."""
@@ -73,6 +72,48 @@ class BaseLlm(BaseModel):
73
72
  )
74
73
  yield # AsyncGenerator requires a yield statement in function body.
75
74
 
75
+ def _maybe_append_user_content(self, llm_request: LlmRequest):
76
+ """Appends a user content, so that model can continue to output.
77
+
78
+ Args:
79
+ llm_request: LlmRequest, the request to send to the Gemini model.
80
+ """
81
+ # If no content is provided, append a user content to hint model response
82
+ # using system instruction.
83
+ if not llm_request.contents:
84
+ llm_request.contents.append(
85
+ types.Content(
86
+ role='user',
87
+ parts=[
88
+ types.Part(
89
+ text=(
90
+ 'Handle the requests as specified in the System'
91
+ ' Instruction.'
92
+ )
93
+ )
94
+ ],
95
+ )
96
+ )
97
+ return
98
+
99
+ # Insert a user content to preserve user intent and to avoid empty
100
+ # model response.
101
+ if llm_request.contents[-1].role != 'user':
102
+ llm_request.contents.append(
103
+ types.Content(
104
+ role='user',
105
+ parts=[
106
+ types.Part(
107
+ text=(
108
+ 'Continue processing previous requests as instructed.'
109
+ ' Exit or provide a summary if no more outputs are'
110
+ ' needed.'
111
+ )
112
+ )
113
+ ],
114
+ )
115
+ )
116
+
76
117
  def connect(self, llm_request: LlmRequest) -> BaseLlmConnection:
77
118
  """Creates a live connection to the LLM.
78
119
 
@@ -145,7 +145,20 @@ class GeminiLlmConnection(BaseLlmConnection):
145
145
  yield self.__build_full_text_response(text)
146
146
  text = ''
147
147
  yield llm_response
148
-
148
+ if (
149
+ message.server_content.input_transcription
150
+ and message.server_content.input_transcription.text
151
+ ):
152
+ user_text = message.server_content.input_transcription.text
153
+ parts = [
154
+ types.Part.from_text(
155
+ text=user_text,
156
+ )
157
+ ]
158
+ llm_response = LlmResponse(
159
+ content=types.Content(role='user', parts=parts)
160
+ )
161
+ yield llm_response
149
162
  if (
150
163
  message.server_content.output_transcription
151
164
  and message.server_content.output_transcription.text
@@ -210,48 +210,6 @@ class Gemini(BaseLlm):
210
210
  ) as live_session:
211
211
  yield GeminiLlmConnection(live_session)
212
212
 
213
- def _maybe_append_user_content(self, llm_request: LlmRequest):
214
- """Appends a user content, so that model can continue to output.
215
-
216
- Args:
217
- llm_request: LlmRequest, the request to send to the Gemini model.
218
- """
219
- # If no content is provided, append a user content to hint model response
220
- # using system instruction.
221
- if not llm_request.contents:
222
- llm_request.contents.append(
223
- types.Content(
224
- role='user',
225
- parts=[
226
- types.Part(
227
- text=(
228
- 'Handle the requests as specified in the System'
229
- ' Instruction.'
230
- )
231
- )
232
- ],
233
- )
234
- )
235
- return
236
-
237
- # Insert a user content to preserve user intent and to avoid empty
238
- # model response.
239
- if llm_request.contents[-1].role != 'user':
240
- llm_request.contents.append(
241
- types.Content(
242
- role='user',
243
- parts=[
244
- types.Part(
245
- text=(
246
- 'Continue processing previous requests as instructed.'
247
- ' Exit or provide a summary if no more outputs are'
248
- ' needed.'
249
- )
250
- )
251
- ],
252
- )
253
- )
254
-
255
213
 
256
214
  def _build_function_declaration_log(
257
215
  func_decl: types.FunctionDeclaration,
@@ -172,19 +172,19 @@ def _content_to_message_param(
172
172
  tool_calls = []
173
173
  content_present = False
174
174
  for part in content.parts:
175
- if part.function_call:
176
- tool_calls.append(
177
- ChatCompletionMessageToolCall(
178
- type="function",
179
- id=part.function_call.id,
180
- function=Function(
181
- name=part.function_call.name,
182
- arguments=part.function_call.args,
183
- ),
184
- )
175
+ if part.function_call:
176
+ tool_calls.append(
177
+ ChatCompletionMessageToolCall(
178
+ type="function",
179
+ id=part.function_call.id,
180
+ function=Function(
181
+ name=part.function_call.name,
182
+ arguments=part.function_call.args,
183
+ ),
185
184
  )
186
- elif part.text or part.inline_data:
187
- content_present = True
185
+ )
186
+ elif part.text or part.inline_data:
187
+ content_present = True
188
188
 
189
189
  final_content = message_content if content_present else None
190
190
 
@@ -453,9 +453,9 @@ def _get_completion_inputs(
453
453
  for content in llm_request.contents or []:
454
454
  message_param_or_list = _content_to_message_param(content)
455
455
  if isinstance(message_param_or_list, list):
456
- messages.extend(message_param_or_list)
457
- elif message_param_or_list: # Ensure it's not None before appending
458
- messages.append(message_param_or_list)
456
+ messages.extend(message_param_or_list)
457
+ elif message_param_or_list: # Ensure it's not None before appending
458
+ messages.append(message_param_or_list)
459
459
 
460
460
  if llm_request.config.system_instruction:
461
461
  messages.insert(
@@ -573,7 +573,6 @@ class LiteLlm(BaseLlm):
573
573
  Attributes:
574
574
  model: The name of the LiteLlm model.
575
575
  llm_client: The LLM client to use for the model.
576
- model_config: The model config.
577
576
  """
578
577
 
579
578
  llm_client: LiteLLMClient = Field(default_factory=LiteLLMClient)
@@ -611,7 +610,8 @@ class LiteLlm(BaseLlm):
611
610
  LlmResponse: The model response.
612
611
  """
613
612
 
614
- logger.info(_build_request_log(llm_request))
613
+ self._maybe_append_user_content(llm_request)
614
+ logger.debug(_build_request_log(llm_request))
615
615
 
616
616
  messages, tools = _get_completion_inputs(llm_request)
617
617
 
@@ -37,7 +37,7 @@ class LlmRequest(BaseModel):
37
37
  """
38
38
 
39
39
  model_config = ConfigDict(arbitrary_types_allowed=True)
40
- """The model config."""
40
+ """The pydantic model config."""
41
41
 
42
42
  model: Optional[str] = None
43
43
  """The model name."""
@@ -41,7 +41,7 @@ class LlmResponse(BaseModel):
41
41
  """
42
42
 
43
43
  model_config = ConfigDict(extra='forbid')
44
- """The model config."""
44
+ """The pydantic model config."""
45
45
 
46
46
  content: Optional[types.Content] = None
47
47
  """The content of the response."""
google/adk/runners.py CHANGED
@@ -186,7 +186,7 @@ class Runner:
186
186
  root_agent = self.agent
187
187
 
188
188
  if new_message:
189
- self._append_new_message_to_session(
189
+ await self._append_new_message_to_session(
190
190
  session,
191
191
  new_message,
192
192
  invocation_context,
@@ -199,7 +199,7 @@ class Runner:
199
199
  self.session_service.append_event(session=session, event=event)
200
200
  yield event
201
201
 
202
- def _append_new_message_to_session(
202
+ async def _append_new_message_to_session(
203
203
  self,
204
204
  session: Session,
205
205
  new_message: types.Content,
@@ -225,7 +225,7 @@ class Runner:
225
225
  if part.inline_data is None:
226
226
  continue
227
227
  file_name = f'artifact_{invocation_context.invocation_id}_{i}'
228
- self.artifact_service.save_artifact(
228
+ await self.artifact_service.save_artifact(
229
229
  app_name=self.app_name,
230
230
  user_id=session.user_id,
231
231
  session_id=session.id,
@@ -297,14 +297,14 @@ class Runner:
297
297
  self.session_service.append_event(session=session, event=event)
298
298
  yield event
299
299
 
300
- def close_session(self, session: Session):
300
+ async def close_session(self, session: Session):
301
301
  """Closes a session and adds it to the memory service (experimental feature).
302
302
 
303
303
  Args:
304
304
  session: The session to close.
305
305
  """
306
306
  if self.memory_service:
307
- self.memory_service.add_session_to_memory(session)
307
+ await self.memory_service.add_session_to_memory(session)
308
308
  self.session_service.close_session(session=session)
309
309
 
310
310
  def _find_agent_to_run(
@@ -0,0 +1,43 @@
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
+ """Utility functions for session service."""
16
+
17
+ import base64
18
+ from typing import Any, Optional
19
+
20
+ from google.genai import types
21
+
22
+
23
+ def encode_content(content: types.Content):
24
+ """Encodes a content object to a JSON dictionary."""
25
+ encoded_content = content.model_dump(exclude_none=True)
26
+ for p in encoded_content["parts"]:
27
+ if "inline_data" in p:
28
+ p["inline_data"]["data"] = base64.b64encode(
29
+ p["inline_data"]["data"]
30
+ ).decode("utf-8")
31
+ return encoded_content
32
+
33
+
34
+ def decode_content(
35
+ content: Optional[dict[str, Any]],
36
+ ) -> Optional[types.Content]:
37
+ """Decodes a content object from a JSON dictionary."""
38
+ if not content:
39
+ return None
40
+ for p in content["parts"]:
41
+ if "inline_data" in p:
42
+ p["inline_data"]["data"] = base64.b64decode(p["inline_data"]["data"])
43
+ return types.Content.model_validate(content)
@@ -26,6 +26,7 @@ from .state import State
26
26
 
27
27
  class GetSessionConfig(BaseModel):
28
28
  """The configuration of getting a session."""
29
+
29
30
  num_recent_events: Optional[int] = None
30
31
  after_timestamp: Optional[float] = None
31
32
 
@@ -35,11 +36,13 @@ class ListSessionsResponse(BaseModel):
35
36
 
36
37
  The events and states are not set within each Session object.
37
38
  """
39
+
38
40
  sessions: list[Session] = Field(default_factory=list)
39
41
 
40
42
 
41
43
  class ListEventsResponse(BaseModel):
42
44
  """The response of listing events in a session."""
45
+
43
46
  events: list[Event] = Field(default_factory=list)
44
47
  next_page_token: Optional[str] = None
45
48
 
@@ -11,8 +11,6 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
-
15
- import base64
16
14
  import copy
17
15
  from datetime import datetime
18
16
  import json
@@ -20,13 +18,13 @@ import logging
20
18
  from typing import Any, Optional
21
19
  import uuid
22
20
 
23
- from google.genai import types
24
21
  from sqlalchemy import Boolean
25
22
  from sqlalchemy import delete
26
23
  from sqlalchemy import Dialect
27
24
  from sqlalchemy import ForeignKeyConstraint
28
25
  from sqlalchemy import func
29
26
  from sqlalchemy import Text
27
+ from sqlalchemy.dialects import mysql
30
28
  from sqlalchemy.dialects import postgresql
31
29
  from sqlalchemy.engine import create_engine
32
30
  from sqlalchemy.engine import Engine
@@ -48,6 +46,7 @@ from typing_extensions import override
48
46
  from tzlocal import get_localzone
49
47
 
50
48
  from ..events.event import Event
49
+ from . import _session_util
51
50
  from .base_session_service import BaseSessionService
52
51
  from .base_session_service import GetSessionConfig
53
52
  from .base_session_service import ListEventsResponse
@@ -58,6 +57,9 @@ from .state import State
58
57
 
59
58
  logger = logging.getLogger(__name__)
60
59
 
60
+ DEFAULT_MAX_KEY_LENGTH = 128
61
+ DEFAULT_MAX_VARCHAR_LENGTH = 256
62
+
61
63
 
62
64
  class DynamicJSON(TypeDecorator):
63
65
  """A JSON-like type that uses JSONB on PostgreSQL and TEXT with JSON
@@ -70,15 +72,16 @@ class DynamicJSON(TypeDecorator):
70
72
  def load_dialect_impl(self, dialect: Dialect):
71
73
  if dialect.name == "postgresql":
72
74
  return dialect.type_descriptor(postgresql.JSONB)
73
- else:
74
- return dialect.type_descriptor(Text) # Default to Text for other dialects
75
+ if dialect.name == "mysql":
76
+ # Use LONGTEXT for MySQL to address the data too long issue
77
+ return dialect.type_descriptor(mysql.LONGTEXT)
78
+ return dialect.type_descriptor(Text) # Default to Text for other dialects
75
79
 
76
80
  def process_bind_param(self, value, dialect: Dialect):
77
81
  if value is not None:
78
82
  if dialect.name == "postgresql":
79
83
  return value # JSONB handles dict directly
80
- else:
81
- return json.dumps(value) # Serialize to JSON string for TEXT
84
+ return json.dumps(value) # Serialize to JSON string for TEXT
82
85
  return value
83
86
 
84
87
  def process_result_value(self, value, dialect: Dialect):
@@ -92,17 +95,25 @@ class DynamicJSON(TypeDecorator):
92
95
 
93
96
  class Base(DeclarativeBase):
94
97
  """Base class for database tables."""
98
+
95
99
  pass
96
100
 
97
101
 
98
102
  class StorageSession(Base):
99
103
  """Represents a session stored in the database."""
104
+
100
105
  __tablename__ = "sessions"
101
106
 
102
- app_name: Mapped[str] = mapped_column(String, primary_key=True)
103
- user_id: Mapped[str] = mapped_column(String, primary_key=True)
107
+ app_name: Mapped[str] = mapped_column(
108
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
109
+ )
110
+ user_id: Mapped[str] = mapped_column(
111
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
112
+ )
104
113
  id: Mapped[str] = mapped_column(
105
- String, primary_key=True, default=lambda: str(uuid.uuid4())
114
+ String(DEFAULT_MAX_KEY_LENGTH),
115
+ primary_key=True,
116
+ default=lambda: str(uuid.uuid4()),
106
117
  )
107
118
 
108
119
  state: Mapped[MutableDict[str, Any]] = mapped_column(
@@ -125,16 +136,27 @@ class StorageSession(Base):
125
136
 
126
137
  class StorageEvent(Base):
127
138
  """Represents an event stored in the database."""
139
+
128
140
  __tablename__ = "events"
129
141
 
130
- id: Mapped[str] = mapped_column(String, primary_key=True)
131
- app_name: Mapped[str] = mapped_column(String, primary_key=True)
132
- user_id: Mapped[str] = mapped_column(String, primary_key=True)
133
- session_id: Mapped[str] = mapped_column(String, primary_key=True)
142
+ id: Mapped[str] = mapped_column(
143
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
144
+ )
145
+ app_name: Mapped[str] = mapped_column(
146
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
147
+ )
148
+ user_id: Mapped[str] = mapped_column(
149
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
150
+ )
151
+ session_id: Mapped[str] = mapped_column(
152
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
153
+ )
134
154
 
135
- invocation_id: Mapped[str] = mapped_column(String)
136
- author: Mapped[str] = mapped_column(String)
137
- branch: Mapped[str] = mapped_column(String, nullable=True)
155
+ invocation_id: Mapped[str] = mapped_column(String(DEFAULT_MAX_VARCHAR_LENGTH))
156
+ author: Mapped[str] = mapped_column(String(DEFAULT_MAX_VARCHAR_LENGTH))
157
+ branch: Mapped[str] = mapped_column(
158
+ String(DEFAULT_MAX_VARCHAR_LENGTH), nullable=True
159
+ )
138
160
  timestamp: Mapped[DateTime] = mapped_column(DateTime(), default=func.now())
139
161
  content: Mapped[dict[str, Any]] = mapped_column(DynamicJSON, nullable=True)
140
162
  actions: Mapped[MutableDict[str, Any]] = mapped_column(PickleType)
@@ -147,8 +169,10 @@ class StorageEvent(Base):
147
169
  )
148
170
  partial: Mapped[bool] = mapped_column(Boolean, nullable=True)
149
171
  turn_complete: Mapped[bool] = mapped_column(Boolean, nullable=True)
150
- error_code: Mapped[str] = mapped_column(String, nullable=True)
151
- error_message: Mapped[str] = mapped_column(String, nullable=True)
172
+ error_code: Mapped[str] = mapped_column(
173
+ String(DEFAULT_MAX_VARCHAR_LENGTH), nullable=True
174
+ )
175
+ error_message: Mapped[str] = mapped_column(String(1024), nullable=True)
152
176
  interrupted: Mapped[bool] = mapped_column(Boolean, nullable=True)
153
177
 
154
178
  storage_session: Mapped[StorageSession] = relationship(
@@ -182,9 +206,12 @@ class StorageEvent(Base):
182
206
 
183
207
  class StorageAppState(Base):
184
208
  """Represents an app state stored in the database."""
209
+
185
210
  __tablename__ = "app_states"
186
211
 
187
- app_name: Mapped[str] = mapped_column(String, primary_key=True)
212
+ app_name: Mapped[str] = mapped_column(
213
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
214
+ )
188
215
  state: Mapped[MutableDict[str, Any]] = mapped_column(
189
216
  MutableDict.as_mutable(DynamicJSON), default={}
190
217
  )
@@ -195,10 +222,15 @@ class StorageAppState(Base):
195
222
 
196
223
  class StorageUserState(Base):
197
224
  """Represents a user state stored in the database."""
225
+
198
226
  __tablename__ = "user_states"
199
227
 
200
- app_name: Mapped[str] = mapped_column(String, primary_key=True)
201
- user_id: Mapped[str] = mapped_column(String, primary_key=True)
228
+ app_name: Mapped[str] = mapped_column(
229
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
230
+ )
231
+ user_id: Mapped[str] = mapped_column(
232
+ String(DEFAULT_MAX_KEY_LENGTH), primary_key=True
233
+ )
202
234
  state: Mapped[MutableDict[str, Any]] = mapped_column(
203
235
  MutableDict.as_mutable(DynamicJSON), default={}
204
236
  )
@@ -353,6 +385,7 @@ class DatabaseSessionService(BaseSessionService):
353
385
  else True
354
386
  )
355
387
  .limit(config.num_recent_events if config else None)
388
+ .order_by(StorageEvent.timestamp.asc())
356
389
  .all()
357
390
  )
358
391
 
@@ -383,7 +416,7 @@ class DatabaseSessionService(BaseSessionService):
383
416
  author=e.author,
384
417
  branch=e.branch,
385
418
  invocation_id=e.invocation_id,
386
- content=_decode_content(e.content),
419
+ content=_session_util.decode_content(e.content),
387
420
  actions=e.actions,
388
421
  timestamp=e.timestamp.timestamp(),
389
422
  long_running_tool_ids=e.long_running_tool_ids,
@@ -451,9 +484,11 @@ class DatabaseSessionService(BaseSessionService):
451
484
 
452
485
  if storage_session.update_time.timestamp() > session.last_update_time:
453
486
  raise ValueError(
454
- f"Session last_update_time {session.last_update_time} is later than"
455
- f" the upate_time in storage {storage_session.update_time}"
456
- )
487
+ f"Session last_update_time "
488
+ f"{datetime.fromtimestamp(session.last_update_time):%Y-%m-%d %H:%M:%S} "
489
+ f"is later than the update_time in storage "
490
+ f"{storage_session.update_time:%Y-%m-%d %H:%M:%S}"
491
+ )
457
492
 
458
493
  # Fetch states from storage
459
494
  storage_app_state = sessionFactory.get(
@@ -506,15 +541,7 @@ class DatabaseSessionService(BaseSessionService):
506
541
  interrupted=event.interrupted,
507
542
  )
508
543
  if event.content:
509
- encoded_content = event.content.model_dump(exclude_none=True)
510
- # Workaround for multimodal Content throwing JSON not serializable
511
- # error with SQLAlchemy.
512
- for p in encoded_content["parts"]:
513
- if "inline_data" in p:
514
- p["inline_data"]["data"] = (
515
- base64.b64encode(p["inline_data"]["data"]).decode("utf-8"),
516
- )
517
- storage_event.content = encoded_content
544
+ storage_event.content = _session_util.encode_content(event.content)
518
545
 
519
546
  sessionFactory.add(storage_event)
520
547
 
@@ -538,6 +565,7 @@ class DatabaseSessionService(BaseSessionService):
538
565
  ) -> ListEventsResponse:
539
566
  raise NotImplementedError()
540
567
 
568
+
541
569
  def convert_event(event: StorageEvent) -> Event:
542
570
  """Converts a storage event to an event."""
543
571
  return Event(
@@ -574,14 +602,3 @@ def _merge_state(app_state, user_state, session_state):
574
602
  for key in user_state.keys():
575
603
  merged_state[State.USER_PREFIX + key] = user_state[key]
576
604
  return merged_state
577
-
578
-
579
- def _decode_content(
580
- content: Optional[dict[str, Any]],
581
- ) -> Optional[types.Content]:
582
- if not content:
583
- return None
584
- for p in content["parts"]:
585
- if "inline_data" in p:
586
- p["inline_data"]["data"] = base64.b64decode(p["inline_data"]["data"][0])
587
- return types.Content.model_validate(content)
@@ -95,14 +95,14 @@ class InMemorySessionService(BaseSessionService):
95
95
  copied_session.events = copied_session.events[
96
96
  -config.num_recent_events :
97
97
  ]
98
- elif config.after_timestamp:
99
- i = len(session.events) - 1
98
+ if config.after_timestamp:
99
+ i = len(copied_session.events) - 1
100
100
  while i >= 0:
101
101
  if copied_session.events[i].timestamp < config.after_timestamp:
102
102
  break
103
103
  i -= 1
104
104
  if i >= 0:
105
- copied_session.events = copied_session.events[i:]
105
+ copied_session.events = copied_session.events[i + 1:]
106
106
 
107
107
  return self._merge_state(app_name, user_id, copied_session)
108
108
 
@@ -38,6 +38,7 @@ class Session(BaseModel):
38
38
  extra='forbid',
39
39
  arbitrary_types_allowed=True,
40
40
  )
41
+ """The pydantic model config."""
41
42
 
42
43
  id: str
43
44
  """The unique identifier of the session."""
@@ -14,21 +14,23 @@
14
14
  import logging
15
15
  import re
16
16
  import time
17
- from typing import Any
18
- from typing import Optional
17
+ from typing import Any, Optional
19
18
 
20
- from dateutil.parser import isoparse
19
+ from dateutil import parser
21
20
  from google import genai
22
21
  from typing_extensions import override
23
22
 
24
23
  from ..events.event import Event
25
24
  from ..events.event_actions import EventActions
25
+ from . import _session_util
26
26
  from .base_session_service import BaseSessionService
27
27
  from .base_session_service import GetSessionConfig
28
28
  from .base_session_service import ListEventsResponse
29
29
  from .base_session_service import ListSessionsResponse
30
30
  from .session import Session
31
31
 
32
+
33
+ isoparse = parser.isoparse
32
34
  logger = logging.getLogger(__name__)
33
35
 
34
36
 
@@ -289,7 +291,7 @@ def _convert_event_to_json(event: Event):
289
291
  }
290
292
  event_json['actions'] = actions_json
291
293
  if event.content:
292
- event_json['content'] = event.content.model_dump(exclude_none=True)
294
+ event_json['content'] = _session_util.encode_content(event.content)
293
295
  if event.error_code:
294
296
  event_json['error_code'] = event.error_code
295
297
  if event.error_message:
@@ -316,7 +318,7 @@ def _from_api_event(api_event: dict) -> Event:
316
318
  invocation_id=api_event['invocationId'],
317
319
  author=api_event['author'],
318
320
  actions=event_actions,
319
- content=api_event.get('content', None),
321
+ content=_session_util.decode_content(api_event.get('content', None)),
320
322
  timestamp=isoparse(api_event['timestamp']).timestamp(),
321
323
  error_code=api_event.get('errorCode', None),
322
324
  error_message=api_event.get('errorMessage', None),
@@ -146,18 +146,21 @@ class AgentTool(BaseTool):
146
146
 
147
147
  if runner.artifact_service:
148
148
  # Forward all artifacts to parent session.
149
- for artifact_name in runner.artifact_service.list_artifact_keys(
149
+ artifact_names = await runner.artifact_service.list_artifact_keys(
150
150
  app_name=session.app_name,
151
151
  user_id=session.user_id,
152
152
  session_id=session.id,
153
- ):
154
- if artifact := runner.artifact_service.load_artifact(
153
+ )
154
+ for artifact_name in artifact_names:
155
+ if artifact := await runner.artifact_service.load_artifact(
155
156
  app_name=session.app_name,
156
157
  user_id=session.user_id,
157
158
  session_id=session.id,
158
159
  filename=artifact_name,
159
160
  ):
160
- tool_context.save_artifact(filename=artifact_name, artifact=artifact)
161
+ await tool_context.save_artifact(
162
+ filename=artifact_name, artifact=artifact
163
+ )
161
164
 
162
165
  if (
163
166
  not last_event