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.
- google/adk/agents/active_streaming_tool.py +1 -0
- google/adk/agents/base_agent.py +27 -29
- google/adk/agents/callback_context.py +4 -4
- google/adk/agents/invocation_context.py +1 -0
- google/adk/agents/langgraph_agent.py +1 -0
- google/adk/agents/live_request_queue.py +1 -0
- google/adk/agents/llm_agent.py +54 -14
- google/adk/agents/run_config.py +4 -0
- google/adk/agents/transcription_entry.py +1 -0
- google/adk/artifacts/base_artifact_service.py +5 -10
- google/adk/artifacts/gcs_artifact_service.py +8 -8
- google/adk/artifacts/in_memory_artifact_service.py +5 -5
- google/adk/auth/auth_credential.py +4 -5
- google/adk/cli/browser/index.html +1 -1
- google/adk/cli/browser/{main-HWIBUY2R.js → main-ULN5R5I5.js} +40 -39
- google/adk/cli/cli.py +54 -47
- google/adk/cli/cli_eval.py +13 -11
- google/adk/cli/cli_tools_click.py +58 -7
- google/adk/cli/fast_api.py +11 -11
- google/adk/cli/fast_api.py.orig +728 -0
- google/adk/evaluation/agent_evaluator.py +3 -3
- google/adk/evaluation/evaluation_constants.py +1 -0
- google/adk/evaluation/evaluation_generator.py +5 -5
- google/adk/evaluation/response_evaluator.py +1 -1
- google/adk/events/event.py +1 -0
- google/adk/events/event_actions.py +10 -4
- google/adk/examples/example.py +1 -0
- google/adk/flows/__init__.py +0 -1
- google/adk/flows/llm_flows/_code_execution.py +10 -10
- google/adk/flows/llm_flows/base_llm_flow.py +40 -15
- google/adk/flows/llm_flows/basic.py +3 -0
- google/adk/flows/llm_flows/contents.py +9 -5
- google/adk/flows/llm_flows/functions.py +38 -16
- google/adk/flows/llm_flows/instructions.py +17 -6
- google/adk/memory/base_memory_service.py +4 -2
- google/adk/memory/in_memory_memory_service.py +2 -2
- google/adk/memory/vertex_ai_rag_memory_service.py +2 -2
- google/adk/models/anthropic_llm.py +20 -2
- google/adk/models/base_llm.py +45 -4
- google/adk/models/gemini_llm_connection.py +14 -1
- google/adk/models/google_llm.py +0 -42
- google/adk/models/lite_llm.py +17 -17
- google/adk/models/llm_request.py +1 -1
- google/adk/models/llm_response.py +1 -1
- google/adk/runners.py +5 -5
- google/adk/sessions/_session_util.py +43 -0
- google/adk/sessions/base_session_service.py +3 -0
- google/adk/sessions/database_session_service.py +63 -46
- google/adk/sessions/in_memory_session_service.py +3 -3
- google/adk/sessions/session.py +1 -0
- google/adk/sessions/vertex_ai_session_service.py +7 -5
- google/adk/tools/agent_tool.py +7 -4
- google/adk/tools/application_integration_tool/__init__.py +2 -0
- google/adk/tools/application_integration_tool/application_integration_toolset.py +48 -26
- google/adk/tools/application_integration_tool/clients/connections_client.py +33 -77
- google/adk/tools/application_integration_tool/integration_connector_tool.py +159 -0
- google/adk/tools/function_tool.py +42 -0
- google/adk/tools/load_artifacts_tool.py +4 -4
- google/adk/tools/load_memory_tool.py +4 -2
- google/adk/tools/mcp_tool/conversion_utils.py +1 -1
- google/adk/tools/mcp_tool/mcp_session_manager.py +14 -0
- google/adk/tools/openapi_tool/common/common.py +2 -5
- google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +13 -3
- google/adk/tools/preload_memory_tool.py +1 -1
- google/adk/tools/tool_context.py +4 -4
- google/adk/version.py +1 -1
- {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/METADATA +3 -7
- {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/RECORD +71 -68
- {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/WHEEL +0 -0
- {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/entry_points.txt +0 -0
- {google_adk-0.3.0.dist-info → google_adk-0.5.0.dist-info}/licenses/LICENSE +0 -0
google/adk/models/base_llm.py
CHANGED
@@ -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
|
google/adk/models/google_llm.py
CHANGED
@@ -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,
|
google/adk/models/lite_llm.py
CHANGED
@@ -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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
187
|
-
|
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
|
-
|
457
|
-
elif message_param_or_list:
|
458
|
-
|
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
|
-
|
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
|
|
google/adk/models/llm_request.py
CHANGED
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
|
-
|
74
|
-
|
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
|
-
|
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(
|
103
|
-
|
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
|
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(
|
131
|
-
|
132
|
-
|
133
|
-
|
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(
|
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(
|
151
|
-
|
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(
|
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(
|
201
|
-
|
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=
|
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
|
-
|
455
|
-
|
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
|
-
|
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
|
-
|
99
|
-
i = len(
|
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
|
|
google/adk/sessions/session.py
CHANGED
@@ -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
|
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
|
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),
|
google/adk/tools/agent_tool.py
CHANGED
@@ -146,18 +146,21 @@ class AgentTool(BaseTool):
|
|
146
146
|
|
147
147
|
if runner.artifact_service:
|
148
148
|
# Forward all artifacts to parent session.
|
149
|
-
|
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
|
-
|
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(
|
161
|
+
await tool_context.save_artifact(
|
162
|
+
filename=artifact_name, artifact=artifact
|
163
|
+
)
|
161
164
|
|
162
165
|
if (
|
163
166
|
not last_event
|