diff --git a/contributing/samples/gepa/experiment.py b/contributing/samples/gepa/experiment.py index 2f5d03a772..f68b349d9c 100644 --- a/contributing/samples/gepa/experiment.py +++ b/contributing/samples/gepa/experiment.py @@ -43,7 +43,6 @@ from tau_bench.types import EnvRunResult from tau_bench.types import RunConfig import tau_bench_agent as tau_bench_agent_lib - import utils diff --git a/contributing/samples/gepa/run_experiment.py b/contributing/samples/gepa/run_experiment.py index cfd850b3a3..1bc4ee58c8 100644 --- a/contributing/samples/gepa/run_experiment.py +++ b/contributing/samples/gepa/run_experiment.py @@ -25,7 +25,6 @@ from absl import flags import experiment from google.genai import types - import utils _OUTPUT_DIR = flags.DEFINE_string( diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index a352918211..5f73d432dd 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -361,7 +361,10 @@ def to_event(self) -> Event: branch=self.branch, # This is needed as previous ADK version pickled actions might not have # value defined in the current version of the EventActions model. - actions=EventActions().model_copy(update=self.actions.model_dump()), + # Use model_validate to properly reconstruct nested Pydantic models. + actions=EventActions.model_validate(self.actions.model_dump()) + if self.actions + else None, timestamp=self.timestamp.timestamp(), long_running_tool_ids=self.long_running_tool_ids, partial=self.partial, diff --git a/tests/unittests/sessions/test_session_service.py b/tests/unittests/sessions/test_session_service.py index 45aa3feede..1946d1f745 100644 --- a/tests/unittests/sessions/test_session_service.py +++ b/tests/unittests/sessions/test_session_service.py @@ -19,6 +19,7 @@ from google.adk.errors.already_exists_error import AlreadyExistsError from google.adk.events.event import Event from google.adk.events.event_actions import EventActions +from google.adk.events.event_actions import EventCompaction from google.adk.sessions.base_session_service import GetSessionConfig from google.adk.sessions.database_session_service import DatabaseSessionService from google.adk.sessions.in_memory_session_service import InMemorySessionService @@ -699,3 +700,67 @@ async def test_partial_events_are_not_persisted(service_type, tmp_path): app_name=app_name, user_id=user_id, session_id=session.id ) assert len(session_got.events) == 0 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'service_type', + [ + SessionServiceType.IN_MEMORY, + SessionServiceType.DATABASE, + SessionServiceType.SQLITE, + ], +) +async def test_event_compaction_deserialization(service_type, tmp_path): + """Test that EventCompaction is properly deserialized as a Pydantic model. + + This test verifies the fix for https://github.com/google/adk-python/issues/3633 + where EventCompaction was incorrectly deserialized as a dict instead of a + Pydantic model when using DatabaseSessionService. + """ + session_service = get_session_service(service_type, tmp_path) + app_name = 'my_app' + user_id = 'user' + + session = await session_service.create_session( + app_name=app_name, user_id=user_id + ) + + # Create an event with EventCompaction + compaction = EventCompaction( + start_timestamp=1.0, + end_timestamp=2.0, + compacted_content=types.Content( + role='model', parts=[types.Part(text='Compacted summary')] + ), + ) + + event = Event( + invocation_id='inv1', + author='user', + actions=EventActions(compaction=compaction), + ) + + await session_service.append_event(session=session, event=event) + + # Retrieve the session and verify compaction is properly deserialized + session_got = await session_service.get_session( + app_name=app_name, user_id=user_id, session_id=session.id + ) + + assert len(session_got.events) == 1 + retrieved_event = session_got.events[0] + + # Verify that compaction is an EventCompaction instance, not a dict + assert retrieved_event.actions is not None + assert retrieved_event.actions.compaction is not None + assert isinstance( + retrieved_event.actions.compaction, EventCompaction + ), f'Expected EventCompaction, got {type(retrieved_event.actions.compaction)}' + + # Verify we can access attributes (not dict keys) + assert retrieved_event.actions.compaction.start_timestamp == 1.0 + assert retrieved_event.actions.compaction.end_timestamp == 2.0 + assert retrieved_event.actions.compaction.compacted_content.parts[0].text == ( + 'Compacted summary' + )