Skip to content

Commit 80a7ecf

Browse files
DeanChensjcopybara-github
authored andcommitted
chore(session): refine raw_event storage with safety fixes and fallback test
- Remove outdated TODOs regarding requested_tool_confirmations and native compaction support - Add a warning log when falling back to legacy storage - Avoid overwriting valid timestamps from `raw_event` with `None` if the API returns `None` - Provide a fallback timestamp (current time) to prevent Pydantic validation errors when the API returns no timestamp Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 899195056
1 parent a64a8e4 commit 80a7ecf

File tree

2 files changed

+81
-26
lines changed

2 files changed

+81
-26
lines changed

src/google/adk/sessions/vertex_ai_session_service.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ async def append_event(self, session: Session, event: Event) -> Event:
270270

271271
reasoning_engine_id = self._get_reasoning_engine_id(session.app_name)
272272

273+
# Build config (Monolithic approach)
273274
config = {}
274275
if event.content:
275276
config['content'] = event.content.model_dump(
@@ -286,9 +287,6 @@ async def append_event(self, session: Session, event: Event) -> Event:
286287
k: json.loads(v.model_dump_json(exclude_none=True, by_alias=True))
287288
for k, v in event.actions.requested_auth_configs.items()
288289
},
289-
# TODO: add requested_tool_confirmations, agent_state once
290-
# they are available in the API.
291-
# Note: compaction is stored via event_metadata.custom_metadata.
292290
}
293291
if event.error_code:
294292
config['error_code'] = event.error_code
@@ -311,10 +309,8 @@ async def append_event(self, session: Session, event: Event) -> Event:
311309
metadata_dict['grounding_metadata'] = event.grounding_metadata.model_dump(
312310
exclude_none=True, mode='json'
313311
)
314-
# Store compaction data in custom_metadata since the Vertex AI service
315-
# does not yet support the compaction field.
316-
# TODO: Stop writing to custom_metadata once the Vertex AI service
317-
# supports the compaction field natively in EventActions.
312+
313+
# ALWAYS write to custom_metadata
318314
if event.actions and event.actions.compaction:
319315
compaction_dict = event.actions.compaction.model_dump(
320316
exclude_none=True, mode='json'
@@ -324,8 +320,6 @@ async def append_event(self, session: Session, event: Event) -> Event:
324320
key=_COMPACTION_CUSTOM_METADATA_KEY,
325321
value=compaction_dict,
326322
)
327-
# Store usage_metadata in custom_metadata since the Vertex AI service
328-
# does not persist it in EventMetadata.
329323
if event.usage_metadata:
330324
usage_dict = event.usage_metadata.model_dump(
331325
exclude_none=True, mode='json'
@@ -335,7 +329,12 @@ async def append_event(self, session: Session, event: Event) -> Event:
335329
key=_USAGE_METADATA_CUSTOM_METADATA_KEY,
336330
value=usage_dict,
337331
)
332+
338333
config['event_metadata'] = metadata_dict
334+
335+
# Persist the full event state using raw_event. If the client-side SDK
336+
# does not support this field, it will raise a ValidationError, and we
337+
# will fall back to legacy field-based storage.
339338
config['raw_event'] = event.model_dump(
340339
exclude_none=True,
341340
mode='json',
@@ -345,7 +344,8 @@ async def append_event(self, session: Session, event: Event) -> Event:
345344
# Retry without raw_event if client side validation fails for older SDK
346345
# versions.
347346
async with self._get_api_client() as api_client:
348-
try:
347+
348+
async def _do_append(cfg: dict[str, Any]):
349349
await api_client.agent_engines.sessions.events.append(
350350
name=(
351351
f'reasoningEngines/{reasoning_engine_id}/sessions/{session.id}'
@@ -355,22 +355,16 @@ async def append_event(self, session: Session, event: Event) -> Event:
355355
timestamp=datetime.datetime.fromtimestamp(
356356
event.timestamp, tz=datetime.timezone.utc
357357
),
358-
config=config,
358+
config=cfg,
359359
)
360+
361+
try:
362+
await _do_append(config)
360363
except pydantic.ValidationError:
364+
logger.warning('Vertex SDK does not support raw_event, falling back.')
361365
if 'raw_event' in config:
362366
del config['raw_event']
363-
await api_client.agent_engines.sessions.events.append(
364-
name=(
365-
f'reasoningEngines/{reasoning_engine_id}/sessions/{session.id}'
366-
),
367-
author=event.author,
368-
invocation_id=event.invocation_id,
369-
timestamp=datetime.datetime.fromtimestamp(
370-
event.timestamp, tz=datetime.timezone.utc
371-
),
372-
config=config,
373-
)
367+
await _do_append(config)
374368
return event
375369

376370
def _get_reasoning_engine_id(self, app_name: str):
@@ -429,8 +423,8 @@ def _get_raw_event(api_event_obj: Any) -> dict[str, Any] | None:
429423

430424
def _from_api_event(api_event_obj: vertexai.types.SessionEvent) -> Event:
431425
"""Converts an API event object to an Event object."""
432-
# Read event data from raw_event first before falling back to top level
433-
# fields.
426+
# Prioritize reading from raw_event to restore full state. Fall back to
427+
# top-level fields for older data that lacks raw_event.
434428
raw_event_dict = _get_raw_event(api_event_obj)
435429
if raw_event_dict:
436430
event_dict = copy.deepcopy(raw_event_dict)
@@ -439,8 +433,9 @@ def _from_api_event(api_event_obj: vertexai.types.SessionEvent) -> Event:
439433
'id': api_event_obj.name.split('/')[-1],
440434
'invocation_id': getattr(api_event_obj, 'invocation_id', None),
441435
'author': getattr(api_event_obj, 'author', None),
442-
'timestamp': timestamp_obj.timestamp() if timestamp_obj else None,
443436
})
437+
if timestamp_obj:
438+
event_dict['timestamp'] = timestamp_obj.timestamp()
444439
return Event.model_validate(event_dict)
445440

446441
actions = getattr(api_event_obj, 'actions', None)
@@ -514,6 +509,13 @@ def _from_api_event(api_event_obj: vertexai.types.SessionEvent) -> Event:
514509
usage_metadata_data
515510
)
516511

512+
timestamp_obj = getattr(api_event_obj, 'timestamp', None)
513+
timestamp = (
514+
timestamp_obj.timestamp()
515+
if timestamp_obj
516+
else datetime.datetime.now(datetime.timezone.utc).timestamp()
517+
)
518+
517519
return Event(
518520
id=api_event_obj.name.split('/')[-1],
519521
invocation_id=api_event_obj.invocation_id,
@@ -522,7 +524,7 @@ def _from_api_event(api_event_obj: vertexai.types.SessionEvent) -> Event:
522524
content=_session_util.decode_model(
523525
getattr(api_event_obj, 'content', None), types.Content
524526
),
525-
timestamp=api_event_obj.timestamp.timestamp(),
527+
timestamp=timestamp,
526528
error_code=getattr(api_event_obj, 'error_code', None),
527529
error_message=getattr(api_event_obj, 'error_message', None),
528530
partial=partial,

tests/unittests/sessions/test_vertex_ai_session_service.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from google.api_core import exceptions as api_core_exceptions
3636
from google.genai import types as genai_types
3737
from google.genai.errors import ClientError
38+
import pydantic
3839
import pytest
3940

4041
MOCK_SESSION_JSON_1 = {
@@ -1223,3 +1224,55 @@ async def test_append_event_with_usage_metadata_and_compaction():
12231224
assert appended_event.custom_metadata == {'extra': 'info'}
12241225
assert '_compaction' not in (appended_event.custom_metadata or {})
12251226
assert '_usage_metadata' not in (appended_event.custom_metadata or {})
1227+
1228+
1229+
@pytest.mark.asyncio
1230+
@pytest.mark.usefixtures('mock_get_api_client')
1231+
async def test_append_event_fallback_for_older_sdk(mock_api_client_instance):
1232+
"""Tests that append_event falls back to custom_metadata when SDK fails on raw_event."""
1233+
session_service = mock_vertex_ai_session_service()
1234+
session = await session_service.get_session(
1235+
app_name='123', user_id='user', session_id='1'
1236+
)
1237+
assert session is not None
1238+
1239+
compaction = EventCompaction(
1240+
start_timestamp=1000.0,
1241+
end_timestamp=2000.0,
1242+
compacted_content=genai_types.Content(
1243+
parts=[genai_types.Part(text='compacted summary')]
1244+
),
1245+
)
1246+
event_to_append = Event(
1247+
invocation_id='fallback_invocation',
1248+
author='model',
1249+
timestamp=1734005534.0,
1250+
actions=EventActions(compaction=compaction),
1251+
)
1252+
1253+
mock_client = mock_api_client_instance
1254+
1255+
async def side_effect(name, author, invocation_id, timestamp, config):
1256+
if 'raw_event' in config:
1257+
# Trigger a real ValidationError since Pydantic V2 doesn't allow easy
1258+
# instantiation
1259+
class DummyModel(pydantic.BaseModel):
1260+
a: int
1261+
1262+
DummyModel(a='not an int')
1263+
return await mock_client._append_event(
1264+
name, author, invocation_id, timestamp, config
1265+
)
1266+
1267+
mock_client.agent_engines.sessions.events.append.side_effect = side_effect
1268+
1269+
await session_service.append_event(session, event_to_append)
1270+
1271+
# Verify that it was written and restored correctly via custom_metadata
1272+
retrieved_session = await session_service.get_session(
1273+
app_name='123', user_id='user', session_id='1'
1274+
)
1275+
appended_event = retrieved_session.events[-1]
1276+
1277+
assert appended_event.actions.compaction is not None
1278+
assert appended_event.actions.compaction.start_timestamp == 1000.0

0 commit comments

Comments
 (0)