|
19 | 19 | from google.adk.agents.base_agent import BaseAgent |
20 | 20 | from google.adk.apps.app import App |
21 | 21 | from google.adk.apps.app import EventsCompactionConfig |
| 22 | +from google.adk.apps.base_events_summarizer import BaseEventsSummarizer |
22 | 23 | from google.adk.apps.compaction import _run_compaction_for_sliding_window |
23 | 24 | import google.adk.apps.compaction as compaction_module |
24 | 25 | from google.adk.apps.llm_event_summarizer import LlmEventSummarizer |
|
31 | 32 | from google.genai import types |
32 | 33 | from google.genai.types import Content |
33 | 34 | from google.genai.types import Part |
| 35 | +from opentelemetry.sdk.trace import TracerProvider |
| 36 | +from opentelemetry.sdk.trace.export import SimpleSpanProcessor |
| 37 | +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter |
34 | 38 | from pydantic import ValidationError |
35 | 39 | import pytest |
36 | 40 |
|
37 | 41 |
|
| 42 | +class _StubSummarizer(BaseEventsSummarizer): |
| 43 | + |
| 44 | + def __init__(self, compacted_event: Event | None): |
| 45 | + self._compacted_event = compacted_event |
| 46 | + |
| 47 | + async def maybe_summarize_events( |
| 48 | + self, *, events: list[Event] |
| 49 | + ) -> Event | None: |
| 50 | + del events |
| 51 | + return self._compacted_event |
| 52 | + |
| 53 | + |
| 54 | +def _create_trace_test_event( |
| 55 | + *, |
| 56 | + timestamp: float, |
| 57 | + invocation_id: str, |
| 58 | + text: str, |
| 59 | + prompt_token_count: int | None = None, |
| 60 | +) -> Event: |
| 61 | + usage_metadata = None |
| 62 | + if prompt_token_count is not None: |
| 63 | + usage_metadata = types.GenerateContentResponseUsageMetadata( |
| 64 | + prompt_token_count=prompt_token_count |
| 65 | + ) |
| 66 | + return Event( |
| 67 | + timestamp=timestamp, |
| 68 | + invocation_id=invocation_id, |
| 69 | + author='user', |
| 70 | + content=Content(role='user', parts=[Part(text=text)]), |
| 71 | + usage_metadata=usage_metadata, |
| 72 | + ) |
| 73 | + |
| 74 | + |
| 75 | +def _create_trace_compacted_event( |
| 76 | + *, start_ts: float, end_ts: float, summary_text: str |
| 77 | +) -> Event: |
| 78 | + compaction = EventCompaction( |
| 79 | + start_timestamp=start_ts, |
| 80 | + end_timestamp=end_ts, |
| 81 | + compacted_content=Content(role='model', parts=[Part(text=summary_text)]), |
| 82 | + ) |
| 83 | + return Event( |
| 84 | + id='compacted-event-id', |
| 85 | + timestamp=end_ts, |
| 86 | + author='compactor', |
| 87 | + content=compaction.compacted_content, |
| 88 | + actions=EventActions(compaction=compaction), |
| 89 | + invocation_id='compacted-invocation-id', |
| 90 | + ) |
| 91 | + |
| 92 | + |
| 93 | +@pytest.fixture |
| 94 | +def span_exporter(monkeypatch: pytest.MonkeyPatch) -> InMemorySpanExporter: |
| 95 | + tracer_provider = TracerProvider() |
| 96 | + span_exporter = InMemorySpanExporter() |
| 97 | + tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter)) |
| 98 | + real_tracer = tracer_provider.get_tracer(__name__) |
| 99 | + monkeypatch.setattr( |
| 100 | + compaction_module.tracer, |
| 101 | + 'start_as_current_span', |
| 102 | + real_tracer.start_as_current_span, |
| 103 | + ) |
| 104 | + return span_exporter |
| 105 | + |
| 106 | + |
38 | 107 | @pytest.mark.parametrize( |
39 | 108 | 'env_variables', ['GOOGLE_AI', 'VERTEX'], indirect=True |
40 | 109 | ) |
@@ -923,6 +992,128 @@ def test_get_contents_compaction_at_beginning(self): |
923 | 992 | actual_texts = [c.parts[0].text for c in result_contents] |
924 | 993 | self.assertEqual(actual_texts, expected_texts) |
925 | 994 |
|
| 995 | + |
| 996 | +@pytest.mark.asyncio |
| 997 | +async def test_run_compaction_for_token_threshold_adds_summary_trace( |
| 998 | + span_exporter: InMemorySpanExporter, |
| 999 | +): |
| 1000 | + session = Session( |
| 1001 | + app_name='app', |
| 1002 | + user_id='user', |
| 1003 | + id='session-id', |
| 1004 | + events=[ |
| 1005 | + _create_trace_test_event( |
| 1006 | + timestamp=1.0, invocation_id='inv1', text='e1' |
| 1007 | + ), |
| 1008 | + _create_trace_test_event( |
| 1009 | + timestamp=2.0, invocation_id='inv2', text='e2' |
| 1010 | + ), |
| 1011 | + _create_trace_test_event( |
| 1012 | + timestamp=3.0, |
| 1013 | + invocation_id='inv3', |
| 1014 | + text='e3', |
| 1015 | + prompt_token_count=100, |
| 1016 | + ), |
| 1017 | + ], |
| 1018 | + ) |
| 1019 | + session_service = AsyncMock(spec=BaseSessionService) |
| 1020 | + compacted_event = _create_trace_compacted_event( |
| 1021 | + start_ts=1.0, end_ts=2.0, summary_text='summary' |
| 1022 | + ) |
| 1023 | + summarizer = _StubSummarizer(compacted_event) |
| 1024 | + config = EventsCompactionConfig( |
| 1025 | + summarizer=summarizer, |
| 1026 | + compaction_interval=999, |
| 1027 | + overlap_size=0, |
| 1028 | + token_threshold=50, |
| 1029 | + event_retention_size=1, |
| 1030 | + ) |
| 1031 | + |
| 1032 | + compacted = ( |
| 1033 | + await ( |
| 1034 | + compaction_module._run_compaction_for_token_threshold_config( |
| 1035 | + config=config, |
| 1036 | + session=session, |
| 1037 | + session_service=session_service, |
| 1038 | + agent=Mock(spec=BaseAgent), |
| 1039 | + ) |
| 1040 | + ) |
| 1041 | + ) |
| 1042 | + |
| 1043 | + assert compacted is True |
| 1044 | + spans = span_exporter.get_finished_spans() |
| 1045 | + summary_span = next( |
| 1046 | + span for span in spans if span.name == 'compact_events token_threshold' |
| 1047 | + ) |
| 1048 | + assert summary_span.attributes['gen_ai.conversation.id'] == 'session-id' |
| 1049 | + assert ( |
| 1050 | + summary_span.attributes['gen_ai.compaction.trigger'] == 'token_threshold' |
| 1051 | + ) |
| 1052 | + assert summary_span.attributes['gen_ai.compaction.event_count'] == 2 |
| 1053 | + assert summary_span.attributes['gen_ai.compaction.token_threshold'] == 50 |
| 1054 | + assert summary_span.attributes['gen_ai.compaction.event_retention_size'] == 1 |
| 1055 | + assert ( |
| 1056 | + summary_span.attributes['gen_ai.compaction.result_event_id'] |
| 1057 | + == 'compacted-event-id' |
| 1058 | + ) |
| 1059 | + |
| 1060 | + |
| 1061 | +@pytest.mark.asyncio |
| 1062 | +async def test_run_compaction_for_sliding_window_adds_summary_trace( |
| 1063 | + span_exporter: InMemorySpanExporter, |
| 1064 | +): |
| 1065 | + compacted_event = _create_trace_compacted_event( |
| 1066 | + start_ts=1.0, end_ts=4.0, summary_text='summary' |
| 1067 | + ) |
| 1068 | + summarizer = _StubSummarizer(compacted_event) |
| 1069 | + app = App( |
| 1070 | + name='test', |
| 1071 | + root_agent=Mock(spec=BaseAgent), |
| 1072 | + events_compaction_config=EventsCompactionConfig( |
| 1073 | + summarizer=summarizer, |
| 1074 | + compaction_interval=2, |
| 1075 | + overlap_size=1, |
| 1076 | + ), |
| 1077 | + ) |
| 1078 | + session = Session( |
| 1079 | + app_name='test', |
| 1080 | + user_id='u1', |
| 1081 | + id='session-id', |
| 1082 | + events=[ |
| 1083 | + _create_trace_test_event( |
| 1084 | + timestamp=1.0, invocation_id='inv1', text='e1' |
| 1085 | + ), |
| 1086 | + _create_trace_test_event( |
| 1087 | + timestamp=2.0, invocation_id='inv2', text='e2' |
| 1088 | + ), |
| 1089 | + _create_trace_test_event( |
| 1090 | + timestamp=3.0, invocation_id='inv3', text='e3' |
| 1091 | + ), |
| 1092 | + _create_trace_test_event( |
| 1093 | + timestamp=4.0, invocation_id='inv4', text='e4' |
| 1094 | + ), |
| 1095 | + ], |
| 1096 | + ) |
| 1097 | + session_service = AsyncMock(spec=BaseSessionService) |
| 1098 | + |
| 1099 | + await _run_compaction_for_sliding_window(app, session, session_service) |
| 1100 | + |
| 1101 | + spans = span_exporter.get_finished_spans() |
| 1102 | + summary_span = next( |
| 1103 | + span for span in spans if span.name == 'compact_events sliding_window' |
| 1104 | + ) |
| 1105 | + assert summary_span.attributes['gen_ai.conversation.id'] == 'session-id' |
| 1106 | + assert ( |
| 1107 | + summary_span.attributes['gen_ai.compaction.trigger'] == 'sliding_window' |
| 1108 | + ) |
| 1109 | + assert summary_span.attributes['gen_ai.compaction.event_count'] == 4 |
| 1110 | + assert summary_span.attributes['gen_ai.compaction.compaction_interval'] == 2 |
| 1111 | + assert summary_span.attributes['gen_ai.compaction.overlap_size'] == 1 |
| 1112 | + assert ( |
| 1113 | + summary_span.attributes['gen_ai.compaction.result_event_id'] |
| 1114 | + == 'compacted-event-id' |
| 1115 | + ) |
| 1116 | + |
926 | 1117 | async def test_sliding_window_excludes_pending_function_call_events(self): |
927 | 1118 | """Sliding-window compaction stops before pending function calls.""" |
928 | 1119 | app = App( |
|
0 commit comments