Skip to content

Commit 66aaec3

Browse files
committed
feature: 更新总结和code execute的功能
- 会话总结增加了角色判断,保证会话总结的是一次完整的用户对话 - code execute 增加了稳定性,避免循环运行tool和代码执行
1 parent 1f39974 commit 66aaec3

15 files changed

Lines changed: 404 additions & 76 deletions

tests/agents/core/test_history_processor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ def test_invocation_mode_filters_by_id(self, invocation_context):
116116
assert len(events) == 1
117117
assert events[0].content.parts[0].text == "current"
118118

119+
def test_invocation_mode_includes_summary_events(self, invocation_context):
120+
proc = HistoryProcessor(timeline_filter_mode=TimelineFilterMode.INVOCATION)
121+
summary_event = _make_event("system", "Previous conversation summary", invocation_id="summary")
122+
summary_event.set_summary_event(True)
123+
current_event = _make_event("user", "current", invocation_id="inv-1")
124+
125+
events = proc.filter_events(invocation_context, [summary_event, current_event])
126+
127+
assert len(events) == 2
128+
assert events[0].is_summary_event()
129+
assert events[1].content.parts[0].text == "current"
130+
119131

120132
# ---------------------------------------------------------------------------
121133
# HistoryProcessor.filter_events - Branch filtering

tests/sessions/test_base_session_service.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ def test_filter_by_num_recent_events(self):
170170
svc = ConcreteSessionService(session_config=config)
171171
session = _make_session()
172172
for i in range(10):
173-
session.events.append(_make_event(text=f"msg{i}"))
173+
author = "user" if i == 7 else "agent"
174+
session.events.append(_make_event(author=author, text=f"msg{i}"))
174175
svc.filter_events(session)
175176
assert len(session.events) == 10
176177
visible_events = [event for event in session.events if event.is_model_visible()]
@@ -185,7 +186,7 @@ def test_filter_by_event_ttl(self):
185186
old_event.timestamp = time.time() - 100
186187
session.events.append(old_event)
187188

188-
new_event = _make_event(text="new")
189+
new_event = _make_event(author="user", text="new")
189190
new_event.timestamp = time.time()
190191
session.events.append(new_event)
191192

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

219+
def test_filter_by_num_recent_events_preserves_summary_anchor(self):
220+
config = SessionServiceConfig(num_recent_events=3)
221+
svc = ConcreteSessionService(session_config=config)
222+
session = _make_session()
223+
224+
summary_event = _make_event(author="system", text="summary")
225+
summary_event.set_summary_event(True)
226+
session.events.append(summary_event)
227+
for i in range(5):
228+
session.events.append(_make_event(text=f"agent{i}"))
229+
230+
svc.filter_events(session)
231+
232+
visible_events = [event for event in session.events if event.is_model_visible()]
233+
assert len(visible_events) == 1
234+
assert visible_events[0].is_summary_event()
235+
218236

219237
class TestBaseSessionServiceSetSummarizerManager:
220238
"""Test set_summarizer_manager method."""

tests/sessions/test_session.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,18 @@ def test_add_event(self):
3535
assert session.events[0].author == "user"
3636
assert session.last_update_time == event.timestamp
3737

38-
def test_is_user_message(self):
39-
"""Test checking if an event is a user message."""
38+
def test_is_anchor_message(self):
39+
"""Test checking if an event can anchor visible conversation history."""
4040
session = Session(id="test-session", app_name="test-app", user_id="test-user", save_key="test-key")
4141

4242
user_event = Event(author="user", content=Content(parts=[Part.from_text(text="Hello")]))
4343
agent_event = Event(author="agent-1", content=Content(parts=[Part.from_text(text="Hi")]))
44+
summary_event = Event(author="system", content=Content(parts=[Part.from_text(text="Summary")]))
45+
summary_event.set_summary_event(True)
4446

45-
assert session._is_user_message(user_event) is True
46-
assert session._is_user_message(agent_event) is False
47+
assert session._is_anchor_message(user_event) is True
48+
assert session._is_anchor_message(agent_event) is False
49+
assert session._is_anchor_message(summary_event) is True
4750

4851
def test_apply_event_filtering_no_config(self):
4952
"""Test event filtering with no configuration."""

tests/sessions/test_session_summarizer.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,107 @@ async def mock_generate(request, stream=False, ctx=None):
344344
visible_events = [event for event in result_events if event.is_model_visible()]
345345
assert len(visible_events) == 4 # 1 summary + 3 recent
346346
assert any(event.is_summary_event() for event in result_events)
347+
summary_event = next(event for event in result_events if event.is_summary_event())
348+
assert summary_event.author == "system"
349+
assert summary_event.content.role == "user"
350+
351+
async def test_summary_filters_visible_events_and_starts_from_user(self):
352+
model = _make_model_mock()
353+
llm_response = MagicMock()
354+
llm_response.content = Content(parts=[Part.from_text(text="summary text")])
355+
captured_prompts = []
356+
357+
async def mock_generate(request, stream=False, ctx=None):
358+
captured_prompts.append(request.contents[0].parts[0].text)
359+
yield llm_response
360+
361+
model.generate_async = mock_generate
362+
summarizer = SessionSummarizer(model=model, start_by_user_turn=True)
363+
hidden_user = _make_event(author="user", text="hidden question")
364+
hidden_user.set_model_visible(False)
365+
first_user = _make_event(author="user", text="visible question")
366+
old_answer = _make_event(author="agent", text="visible answer")
367+
recent_user = _make_event(author="user", text="recent question")
368+
events = [
369+
_make_event(author="system", text="system preamble"),
370+
hidden_user,
371+
first_user,
372+
old_answer,
373+
recent_user,
374+
]
375+
376+
summary_text, result_events = await summarizer.create_session_summary_by_events(
377+
events, "s1", keep_recent_count=1)
378+
379+
assert summary_text == "summary text"
380+
assert result_events is events
381+
assert captured_prompts
382+
assert "visible question" in captured_prompts[0]
383+
assert "visible answer" in captured_prompts[0]
384+
assert "system preamble" not in captured_prompts[0]
385+
assert "hidden question" not in captured_prompts[0]
386+
assert "recent question" not in captured_prompts[0]
387+
assert first_user.is_model_visible() is False
388+
assert old_answer.is_model_visible() is False
389+
assert recent_user.is_model_visible() is True
390+
assert any(event.is_summary_event() for event in result_events)
391+
392+
async def test_summary_can_start_from_existing_summary_event(self):
393+
model = _make_model_mock()
394+
llm_response = MagicMock()
395+
llm_response.content = Content(parts=[Part.from_text(text="summary text")])
396+
captured_prompts = []
397+
398+
async def mock_generate(request, stream=False, ctx=None):
399+
captured_prompts.append(request.contents[0].parts[0].text)
400+
yield llm_response
401+
402+
model.generate_async = mock_generate
403+
summarizer = SessionSummarizer(model=model, start_by_user_turn=True)
404+
existing_summary = _make_event(author="system", text="previous summary")
405+
existing_summary.set_summary_event(True)
406+
events = [
407+
_make_event(author="system", text="system preamble"),
408+
existing_summary,
409+
_make_event(author="agent", text="old answer"),
410+
_make_event(author="user", text="recent question"),
411+
]
412+
413+
summary_text, result_events = await summarizer.create_session_summary_by_events(
414+
events, "s1", keep_recent_count=1)
415+
416+
assert summary_text == "summary text"
417+
assert "previous summary" in captured_prompts[0]
418+
assert "old answer" in captured_prompts[0]
419+
assert "system preamble" not in captured_prompts[0]
420+
assert result_events[1].is_summary_event()
421+
422+
async def test_summary_falls_back_to_first_visible_event_and_ignores_large_keep_recent(self):
423+
model = _make_model_mock()
424+
llm_response = MagicMock()
425+
llm_response.content = Content(parts=[Part.from_text(text="summary text")])
426+
captured_prompts = []
427+
428+
async def mock_generate(request, stream=False, ctx=None):
429+
captured_prompts.append(request.contents[0].parts[0].text)
430+
yield llm_response
431+
432+
model.generate_async = mock_generate
433+
summarizer = SessionSummarizer(model=model, start_by_user_turn=True)
434+
events = [
435+
_make_event(author="agent", text="agent message 1"),
436+
_make_event(author="agent", text="agent message 2"),
437+
]
438+
439+
summary_text, result_events = await summarizer.create_session_summary_by_events(
440+
events, "s1", keep_recent_count=10)
441+
442+
assert summary_text == "summary text"
443+
assert "agent message 1" in captured_prompts[0]
444+
assert "agent message 2" in captured_prompts[0]
445+
visible_events = [event for event in result_events if event.is_model_visible()]
446+
assert len(visible_events) == 1
447+
assert visible_events[0].is_summary_event()
347448

348449
async def test_summary_without_keep_recent(self):
349450
model = _make_model_mock()
@@ -362,6 +463,8 @@ async def mock_generate(request, stream=False, ctx=None):
362463
assert len(result_events) == 6 # preserve all original events + 1 summary
363464
visible_events = [event for event in result_events if event.is_model_visible()]
364465
assert len(visible_events) == 1 # only summary event remains model-visible
466+
assert visible_events[0].is_summary_event()
467+
assert visible_events[0].content.role == "user"
365468

366469
async def test_summary_no_events(self):
367470
model = _make_model_mock()
@@ -407,6 +510,69 @@ async def mock_generate(request, stream=False, ctx=None):
407510
assert len(visible_events) == 3 # 1 summary + 2 recent
408511
assert any(event.is_summary_event() for event in session.events)
409512

513+
async def test_summary_uses_visible_events_starting_from_user(self):
514+
model = _make_model_mock()
515+
llm_response = MagicMock()
516+
llm_response.content = Content(parts=[Part.from_text(text="session summary")])
517+
captured_prompts = []
518+
519+
async def mock_generate(request, stream=False, ctx=None):
520+
captured_prompts.append(request.contents[0].parts[0].text)
521+
yield llm_response
522+
523+
model.generate_async = mock_generate
524+
summarizer = SessionSummarizer(model=model, keep_recent_count=1, start_by_user_turn=True)
525+
hidden_user = _make_event(author="user", text="hidden question")
526+
hidden_user.set_model_visible(False)
527+
first_user = _make_event(author="user", text="visible question")
528+
old_answer = _make_event(author="agent", text="visible answer")
529+
recent_user = _make_event(author="user", text="recent question")
530+
session = _make_session(events=[
531+
_make_event(author="system", text="system preamble"),
532+
hidden_user,
533+
first_user,
534+
old_answer,
535+
recent_user,
536+
])
537+
538+
result = await summarizer.create_session_summary(session)
539+
540+
assert result == "session summary"
541+
assert captured_prompts
542+
assert "visible question" in captured_prompts[0]
543+
assert "visible answer" in captured_prompts[0]
544+
assert "system preamble" not in captured_prompts[0]
545+
assert "hidden question" not in captured_prompts[0]
546+
assert "recent question" not in captured_prompts[0]
547+
assert first_user.is_model_visible() is False
548+
assert old_answer.is_model_visible() is False
549+
assert recent_user.is_model_visible() is True
550+
assert any(event.is_summary_event() for event in session.events)
551+
552+
async def test_summary_without_visible_user_falls_back_to_first_visible_event(self):
553+
model = _make_model_mock()
554+
llm_response = MagicMock()
555+
llm_response.content = Content(parts=[Part.from_text(text="session summary")])
556+
557+
async def mock_generate(request, stream=False, ctx=None):
558+
yield llm_response
559+
560+
model.generate_async = mock_generate
561+
summarizer = SessionSummarizer(model=model, keep_recent_count=10, start_by_user_turn=True)
562+
events = [
563+
_make_event(author="system", text="system preamble"),
564+
_make_event(author="agent", text="agent answer"),
565+
]
566+
session = _make_session(events=events)
567+
568+
result = await summarizer.create_session_summary(session)
569+
570+
assert result == "session summary"
571+
assert len(session.events) == 3
572+
visible_events = [event for event in session.events if event.is_model_visible()]
573+
assert len(visible_events) == 1
574+
assert visible_events[0].is_summary_event()
575+
410576
async def test_summary_no_update_on_failure(self):
411577
model = _make_model_mock()
412578

tests/sessions/test_utils.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,25 @@
1818

1919
import pytest
2020

21+
from trpc_agent_sdk.events import Event
2122
from trpc_agent_sdk.sessions._utils import (
2223
StateStorageEntry,
2324
app_state_key,
2425
extract_state_delta,
26+
find_events_for_summary,
2527
merge_state,
2628
session_key,
2729
user_state_key,
2830
)
31+
from trpc_agent_sdk.types import Content
32+
from trpc_agent_sdk.types import Part
2933
from trpc_agent_sdk.types import State
3034

3135

36+
def _make_event(author: str = "agent", text: str = "hello") -> Event:
37+
return Event(author=author, content=Content(parts=[Part.from_text(text=text)]))
38+
39+
3240
class TestStateStorageEntry:
3341
"""Test StateStorageEntry dataclass."""
3442

@@ -160,6 +168,95 @@ def test_need_copy_false(self):
160168
assert "new_key" in original_session_state
161169

162170

171+
class TestFindEventsForSummary:
172+
"""Test event selection for summary generation."""
173+
174+
def test_defaults_start_from_user_and_keep_recent(self):
175+
events = [
176+
_make_event(author="system", text="system preamble"),
177+
_make_event(author="user", text="question"),
178+
_make_event(author="agent", text="answer"),
179+
_make_event(author="user", text="recent question"),
180+
]
181+
182+
selected_events, start_index = find_events_for_summary(events, keep_recent_count=1)
183+
184+
assert selected_events == events[1:3]
185+
assert start_index == 1
186+
187+
def test_start_from_summary_event(self):
188+
summary_event = _make_event(author="system", text="previous summary")
189+
summary_event.set_summary_event(True)
190+
events = [
191+
_make_event(author="system", text="system preamble"),
192+
summary_event,
193+
_make_event(author="agent", text="answer"),
194+
_make_event(author="user", text="recent question"),
195+
]
196+
197+
selected_events, start_index = find_events_for_summary(events, keep_recent_count=1)
198+
199+
assert selected_events == events[1:3]
200+
assert start_index == 1
201+
202+
def test_fallback_to_first_visible_event(self):
203+
events = [
204+
_make_event(author="agent", text="answer 1"),
205+
_make_event(author="agent", text="answer 2"),
206+
]
207+
208+
selected_events, start_index = find_events_for_summary(events, keep_recent_count=1)
209+
210+
assert selected_events == events[:1]
211+
assert start_index == 0
212+
213+
def test_filters_model_invisible_events(self):
214+
hidden_user = _make_event(author="user", text="hidden")
215+
hidden_user.set_model_visible(False)
216+
events = [
217+
hidden_user,
218+
_make_event(author="user", text="visible question"),
219+
_make_event(author="agent", text="visible answer"),
220+
]
221+
222+
selected_events, start_index = find_events_for_summary(events, keep_recent_count=10)
223+
224+
assert selected_events == events[1:]
225+
assert start_index == 1
226+
227+
def test_ignores_keep_recent_when_it_would_empty_selection(self):
228+
events = [
229+
_make_event(author="user", text="question"),
230+
_make_event(author="agent", text="answer"),
231+
]
232+
233+
selected_events, start_index = find_events_for_summary(events, keep_recent_count=10)
234+
235+
assert selected_events == events
236+
assert start_index == 0
237+
238+
def test_none_keep_recent_selects_all_matching_events(self):
239+
events = [
240+
_make_event(author="system", text="system preamble"),
241+
_make_event(author="user", text="question"),
242+
_make_event(author="agent", text="answer"),
243+
]
244+
245+
selected_events, start_index = find_events_for_summary(events, keep_recent_count=None)
246+
247+
assert selected_events == events[1:]
248+
assert start_index == 1
249+
250+
def test_no_visible_events(self):
251+
event = _make_event(author="user", text="hidden")
252+
event.set_model_visible(False)
253+
254+
selected_events, start_index = find_events_for_summary([event])
255+
256+
assert selected_events == []
257+
assert start_index == -1
258+
259+
163260
class TestKeyFunctions:
164261
"""Test key generation functions."""
165262

0 commit comments

Comments
 (0)