Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions tests/agents/core/test_history_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ def test_invocation_mode_filters_by_id(self, invocation_context):
assert len(events) == 1
assert events[0].content.parts[0].text == "current"

def test_invocation_mode_includes_summary_events(self, invocation_context):
proc = HistoryProcessor(timeline_filter_mode=TimelineFilterMode.INVOCATION)
summary_event = _make_event("system", "Previous conversation summary", invocation_id="summary")
summary_event.set_summary_event(True)
current_event = _make_event("user", "current", invocation_id="inv-1")

events = proc.filter_events(invocation_context, [summary_event, current_event])

assert len(events) == 2
assert events[0].is_summary_event()
assert events[1].content.parts[0].text == "current"


# ---------------------------------------------------------------------------
# HistoryProcessor.filter_events - Branch filtering
Expand Down
22 changes: 20 additions & 2 deletions tests/sessions/test_base_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ def test_filter_by_num_recent_events(self):
svc = ConcreteSessionService(session_config=config)
session = _make_session()
for i in range(10):
session.events.append(_make_event(text=f"msg{i}"))
author = "user" if i == 7 else "agent"
session.events.append(_make_event(author=author, text=f"msg{i}"))
svc.filter_events(session)
assert len(session.events) == 10
visible_events = [event for event in session.events if event.is_model_visible()]
Expand All @@ -185,7 +186,7 @@ def test_filter_by_event_ttl(self):
old_event.timestamp = time.time() - 100
session.events.append(old_event)

new_event = _make_event(text="new")
new_event = _make_event(author="user", text="new")
new_event.timestamp = time.time()
session.events.append(new_event)

Expand Down Expand Up @@ -215,6 +216,23 @@ def test_filter_ttl_removes_all_old(self):
assert len(session.events) == 5
assert all(not event.is_model_visible() for event in session.events)

def test_filter_by_num_recent_events_preserves_summary_anchor(self):
config = SessionServiceConfig(num_recent_events=3)
svc = ConcreteSessionService(session_config=config)
session = _make_session()

summary_event = _make_event(author="system", text="summary")
summary_event.set_summary_event(True)
session.events.append(summary_event)
for i in range(5):
session.events.append(_make_event(text=f"agent{i}"))

svc.filter_events(session)

visible_events = [event for event in session.events if event.is_model_visible()]
assert len(visible_events) == 1
assert visible_events[0].is_summary_event()


class TestBaseSessionServiceSetSummarizerManager:
"""Test set_summarizer_manager method."""
Expand Down
14 changes: 8 additions & 6 deletions tests/sessions/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from google.genai.types import Part
from trpc_agent_sdk.events import Event
from trpc_agent_sdk.sessions import Session
from trpc_agent_sdk.sessions import is_summary_anchor


class TestSession:
Expand All @@ -35,15 +36,16 @@ def test_add_event(self):
assert session.events[0].author == "user"
assert session.last_update_time == event.timestamp

def test_is_user_message(self):
"""Test checking if an event is a user message."""
session = Session(id="test-session", app_name="test-app", user_id="test-user", save_key="test-key")

def test_is_anchor_message(self):
"""Test checking if an event can anchor visible conversation history."""
user_event = Event(author="user", content=Content(parts=[Part.from_text(text="Hello")]))
agent_event = Event(author="agent-1", content=Content(parts=[Part.from_text(text="Hi")]))
summary_event = Event(author="system", content=Content(parts=[Part.from_text(text="Summary")]))
summary_event.set_summary_event(True)

assert session._is_user_message(user_event) is True
assert session._is_user_message(agent_event) is False
assert is_summary_anchor(user_event) is True
assert is_summary_anchor(agent_event) is False
assert is_summary_anchor(summary_event) is True

def test_apply_event_filtering_no_config(self):
"""Test event filtering with no configuration."""
Expand Down
195 changes: 193 additions & 2 deletions tests/sessions/test_session_summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,136 @@ async def mock_generate(request, stream=False, ctx=None):
visible_events = [event for event in result_events if event.is_model_visible()]
assert len(visible_events) == 4 # 1 summary + 3 recent
assert any(event.is_summary_event() for event in result_events)
summary_event = next(event for event in result_events if event.is_summary_event())
assert summary_event.author == "system"
assert summary_event.content.role == "user"

async def test_summary_without_keep_recent(self):
async def test_summary_traces_back_to_invisible_user_before_first_visible_event(self):
model = _make_model_mock()
llm_response = MagicMock()
llm_response.content = Content(parts=[Part.from_text(text="summary text")])
captured_prompts = []

async def mock_generate(request, stream=False, ctx=None):
captured_prompts.append(request.contents[0].parts[0].text)
yield llm_response

model.generate_async = mock_generate
summarizer = SessionSummarizer(model=model, start_by_user_turn=True)
hidden_user = _make_event(author="user", text="hidden question")
hidden_user.set_model_visible(False)
old_answer = _make_event(author="agent", text="visible answer")
recent_user = _make_event(author="user", text="recent question")
system_preamble = _make_event(author="system", text="system preamble")
system_preamble.set_model_visible(False)
events = [
system_preamble,
hidden_user,
old_answer,
recent_user,
]

summary_text, result_events = await summarizer.create_session_summary_by_events(
events, "s1", keep_recent_count=1)

assert summary_text == "summary text"
assert result_events is events
assert captured_prompts
assert "hidden question" in captured_prompts[0]
assert "visible answer" in captured_prompts[0]
assert "system preamble" not in captured_prompts[0]
assert "recent question" not in captured_prompts[0]
assert old_answer.is_model_visible() is False
assert recent_user.is_model_visible() is True
assert any(event.is_summary_event() for event in result_events)

async def test_summary_can_start_from_existing_summary_event(self):
model = _make_model_mock()
llm_response = MagicMock()
llm_response.content = Content(parts=[Part.from_text(text="summary text")])
captured_prompts = []

async def mock_generate(request, stream=False, ctx=None):
captured_prompts.append(request.contents[0].parts[0].text)
yield llm_response

model.generate_async = mock_generate
summarizer = SessionSummarizer(model=model, start_by_user_turn=True)
existing_summary = _make_event(author="system", text="previous summary")
existing_summary.set_summary_event(True)
system_preamble = _make_event(author="system", text="system preamble")
system_preamble.set_model_visible(False)
events = [
system_preamble,
existing_summary,
_make_event(author="agent", text="old answer"),
_make_event(author="user", text="recent question"),
]

summary_text, result_events = await summarizer.create_session_summary_by_events(
events, "s1", keep_recent_count=1)

assert summary_text == "summary text"
assert "previous summary" in captured_prompts[0]
assert "old answer" in captured_prompts[0]
assert "system preamble" not in captured_prompts[0]
assert result_events[3].is_summary_event()

async def test_summary_falls_back_to_first_visible_event_and_ignores_large_keep_recent(self):
model = _make_model_mock()
llm_response = MagicMock()
llm_response.content = Content(parts=[Part.from_text(text="summary text")])
captured_prompts = []

async def mock_generate(request, stream=False, ctx=None):
captured_prompts.append(request.contents[0].parts[0].text)
yield llm_response

model.generate_async = mock_generate
summarizer = SessionSummarizer(model=model, start_by_user_turn=True)
events = [
_make_event(author="agent", text="agent message 1"),
_make_event(author="agent", text="agent message 2"),
]

summary_text, result_events = await summarizer.create_session_summary_by_events(
events, "s1", keep_recent_count=10)

assert summary_text == "summary text"
assert "agent message 1" in captured_prompts[0]
assert "agent message 2" in captured_prompts[0]
visible_events = [event for event in result_events if event.is_model_visible()]
assert len(visible_events) == 1
assert visible_events[0].is_summary_event()

async def test_summary_inserted_before_recent_user_turn_and_hides_prior_events(self):
model = _make_model_mock()
llm_response = MagicMock()
llm_response.content = Content(parts=[Part.from_text(text="summary text")])
captured_prompts = []

async def mock_generate(request, stream=False, ctx=None):
captured_prompts.append(request.contents[0].parts[0].text)
yield llm_response

model.generate_async = mock_generate
summarizer = SessionSummarizer(model=model, start_by_user_turn=True)
events = [_make_event(author="user" if idx in (8, 80, 92) else "agent", text=f"msg {idx}") for idx in range(100)]
for idx, event in enumerate(events):
event.set_model_visible(10 <= idx < 99)

summary_text, result_events = await summarizer.create_session_summary_by_events(
events, "s1", keep_recent_count=10)

assert summary_text == "summary text"
assert "msg 8" in captured_prompts[0]
assert "msg 91" in captured_prompts[0]
assert "msg 92" not in captured_prompts[0]
assert result_events[92].is_summary_event()
assert all(not event.is_model_visible() for event in result_events[:92])
assert result_events[93].is_model_visible()

async def test_summary_with_zero_keep_recent(self):
model = _make_model_mock()
llm_response = MagicMock()
llm_response.content = Content(parts=[Part.from_text(text="summary text")])
Expand All @@ -357,11 +485,13 @@ async def mock_generate(request, stream=False, ctx=None):
summarizer = SessionSummarizer(model=model)
events = [_make_event(text=f"msg{i}") for i in range(5)]
summary_text, result_events = await summarizer.create_session_summary_by_events(
events, "s1", keep_recent_count=None)
events, "s1", keep_recent_count=0)
assert summary_text is not None
assert len(result_events) == 6 # preserve all original events + 1 summary
visible_events = [event for event in result_events if event.is_model_visible()]
assert len(visible_events) == 1 # only summary event remains model-visible
assert visible_events[0].is_summary_event()
assert visible_events[0].content.role == "user"

async def test_summary_no_events(self):
model = _make_model_mock()
Expand Down Expand Up @@ -407,6 +537,67 @@ async def mock_generate(request, stream=False, ctx=None):
assert len(visible_events) == 3 # 1 summary + 2 recent
assert any(event.is_summary_event() for event in session.events)

async def test_summary_traces_back_to_invisible_user_before_visible_events(self):
model = _make_model_mock()
llm_response = MagicMock()
llm_response.content = Content(parts=[Part.from_text(text="session summary")])
captured_prompts = []

async def mock_generate(request, stream=False, ctx=None):
captured_prompts.append(request.contents[0].parts[0].text)
yield llm_response

model.generate_async = mock_generate
summarizer = SessionSummarizer(model=model, keep_recent_count=1, start_by_user_turn=True)
hidden_user = _make_event(author="user", text="hidden question")
hidden_user.set_model_visible(False)
old_answer = _make_event(author="agent", text="visible answer")
recent_user = _make_event(author="user", text="recent question")
system_preamble = _make_event(author="system", text="system preamble")
system_preamble.set_model_visible(False)
session = _make_session(events=[
system_preamble,
hidden_user,
old_answer,
recent_user,
])

result = await summarizer.create_session_summary(session)

assert result == "session summary"
assert captured_prompts
assert "hidden question" in captured_prompts[0]
assert "visible answer" in captured_prompts[0]
assert "system preamble" not in captured_prompts[0]
assert "recent question" not in captured_prompts[0]
assert old_answer.is_model_visible() is False
assert recent_user.is_model_visible() is True
assert any(event.is_summary_event() for event in session.events)

async def test_summary_without_visible_user_falls_back_to_first_visible_event(self):
model = _make_model_mock()
llm_response = MagicMock()
llm_response.content = Content(parts=[Part.from_text(text="session summary")])

async def mock_generate(request, stream=False, ctx=None):
yield llm_response

model.generate_async = mock_generate
summarizer = SessionSummarizer(model=model, keep_recent_count=10, start_by_user_turn=True)
events = [
_make_event(author="system", text="system preamble"),
_make_event(author="agent", text="agent answer"),
]
session = _make_session(events=events)

result = await summarizer.create_session_summary(session)

assert result == "session summary"
assert len(session.events) == 3
visible_events = [event for event in session.events if event.is_model_visible()]
assert len(visible_events) == 1
assert visible_events[0].is_summary_event()

async def test_summary_no_update_on_failure(self):
model = _make_model_mock()

Expand Down
Loading
Loading