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
4 changes: 2 additions & 2 deletions examples/fastapi_server/_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ async def chat(req: ChatRequest) -> ChatResponse: # pylint: disable=unused-vari
))

except Exception as exc:
logger.exception("Error during agent run (session=%s)", session_id)
logger.error("Error during agent run (session=%s): %s", session_id, exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc

return ChatResponse(
Expand Down Expand Up @@ -214,7 +214,7 @@ async def _event_generator() -> AsyncGenerator[str, None]:
yield _sse(StreamChunk(type="done", session_id=session_id))

except Exception as exc:
logger.exception("Error during streaming run (session=%s)", session_id)
logger.error("Error during streaming run (session=%s): %s", session_id, exc)
yield _sse(StreamChunk(type="error", data=str(exc), session_id=session_id))

return StreamingResponse(
Expand Down
39 changes: 15 additions & 24 deletions examples/session_summarizer/run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async def summarize_session(session_service: InMemorySessionService, app_name: s
print(f" - Compression ratio: {summary.get_compression_ratio():.1f}%")


SUMMARIZER_COUNT = 3 # Run summarization every SUMMARIZER_COUNT turns (e.g. 3 => every 3 turns)
SUMMARIZER_COUNT = 2 # Keep the example short: summarize after a couple of turns.


def create_summarizer_manager(model: OpenAIModel) -> SummarizerSessionManager:
Expand All @@ -154,8 +154,8 @@ def create_summarizer_manager(model: OpenAIModel) -> SummarizerSessionManager:
# set_summarizer_time_interval_threshold(10),
# )
],
max_summary_length=600, # Max summary length kept; default 1000; beyond shows ...
keep_recent_count=4, # How many recent turns to keep; default 10
max_summary_length=300, # Max summary length kept; default 1000; beyond shows ...
keep_recent_count=2, # Keep only the latest turns so compression is easy to observe.
)
# Create SummarizerSessionManager
summarizer_manager = SummarizerSessionManager(
Expand All @@ -169,7 +169,7 @@ def create_summarizer_manager(model: OpenAIModel) -> SummarizerSessionManager:
async def llm_agent_summarizer():
"""Demo LlmAgent integrated with SummarizerSessionManager."""
print("=" * 60)
print("Example 2: LlmAgent + SummarizerSessionManager demo")
print("Example: LlmAgent + SummarizerSessionManager demo")
print("=" * 60)
app_name = "llm_summarizer_manager_demo"

Expand All @@ -183,22 +183,13 @@ async def llm_agent_summarizer():
current_session_id = str(uuid.uuid4())
print(f"📊 Session: {app_name}/{user_id}/{current_session_id}")

# Demo conversation turns
# Short demo conversation. Four turns are enough to trigger automatic
# summarization while keeping the example quick to run.
conversations = [
"Hello! I want to learn Python programming. Can you help me?",
"What is a variable? Can you give an example?",
"Got it! What data types are there?",
"What does control flow mean?",
"I understand those ideas. I'd like a small project to practice.",
"OK! How do I build this calculator?",
"The calculator looks good—I ran it successfully. I'd like to learn more advanced Python.",
"I'd like to start with functions—I think they're central to programming.",
"I see—functions make code modular and reusable. I'd like to learn OOP next.",
"I get OOP now. I'd like to learn exception handling.",
"I've learned these advanced topics. I'd like a bigger project that ties them together.",
"Yes! How do I implement this library system?",
"The structure looks good. How do I persist data to files?",
"Great! I've covered basics and advanced topics including files. I'd like a recap of what I learned.",
"Please give me a tiny calculator example.",
"Can you recap what I learned so far?",
]

print(f"\n💬 Multi-turn dialogue ({len(conversations)} turns)...")
Expand Down Expand Up @@ -230,18 +221,18 @@ async def llm_agent_summarizer():
# elif part.text:
# print(f"\n✅ {part.text}")

# After every SUMMARIZER_COUNT turns, inspect session state
if index % SUMMARIZER_COUNT == 0: # summarizer should fire around this cadence
if session:
print(f"\n📊 Session state after turn {index + 1}:")
summary = await session_service.summarizer_manager.get_session_summary(session)
# Inspect the summary after the threshold cadence.
if (index + 1) % SUMMARIZER_COUNT == 0 and session:
print(f"\n📊 Session state after turn {index + 1}:")
summary = await session_service.summarizer_manager.get_session_summary(session)
if summary:
print(f" - Summary text: {summary.summary_text[:100]}...")
print(f" - Original event count: {summary.original_event_count}")
print(f" - Compressed event count: {summary.compressed_event_count}")
print(f" - Compression ratio: {summary.get_compression_ratio()}")
else:
print(" - Summary not created yet.")
print("\n" + "-" * 40)
# Manual forced summary test
await summarize_session(session_service, app_name, user_id, current_session_id)


if __name__ == "__main__":
Expand Down
21 changes: 21 additions & 0 deletions lint_flake8.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash

set -euo pipefail

# Usage:
# bash lint_flake8.sh # check current project
# bash lint_flake8.sh path/to/check # check a specific path

TARGET_PATH="${1:-.}"

if ! command -v flake8 >/dev/null 2>&1; then
echo "flake8 is not installed. Install it first:"
echo " python3 -m pip install flake8"
exit 1
fi

echo "Running flake8 on: ${TARGET_PATH}"

flake8 "${TARGET_PATH}" \
--max-line-length=120 \
--extend-exclude=".git,__pycache__,.pytest_cache,.mypy_cache,.ruff_cache,venv,.venv,build,dist,node_modules"
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ knowledge = [
]

a2a = [
"a2a-sdk>=0.2.0",
"a2a-sdk<1.0.0,>=0.3.22",
"protobuf>=5.29.5",
]

agent-claude = [
"claude-agent-sdk>=0.1.3",
"claude-agent-sdk>=0.1.3,<0.1.64",
"cloudpickle>=2.0.0",
]

Expand Down Expand Up @@ -115,20 +115,19 @@ dev = [
"langchain_community>=0.3.27",
"langchain_huggingface>=0.1.0",
"ag-ui-protocol>=0.1.8",
"claude-agent-sdk>=0.1.3",
"claude-agent-sdk>=0.1.3,<0.1.64",
"cloudpickle>=2.0.0",
"typer>=0.9.0",
]

all = [
"a2a-sdk>=0.2.0",
"protobuf>=5.29.5",
"numpy>=2.2.5",
"langchain_community>=0.3.27",
"langchain_huggingface>=0.1.0",
"langchain_tavily",
"ag-ui-protocol>=0.1.8",
"claude-agent-sdk>=0.1.3",
"claude-agent-sdk>=0.1.3,<0.1.64",
"pytest",
"pytest-asyncio",
"rouge-score",
Expand All @@ -140,6 +139,7 @@ all = [
"nanobot-ai>=0.1.4.post6",
"aiofiles",
"wecom-aibot-sdk-python>=0.1.5",
"a2a-sdk<1.0.0,>=0.3.22",
]

[project.scripts]
Expand Down
4 changes: 4 additions & 0 deletions tests/evaluation/test_eval_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def _make_session():
"""Create a mock session with required attributes."""
session = MagicMock()
session.events = []
def _insert_events(events, idx=None):
insert_idx = 0 if idx is None else idx
session.events[insert_idx:insert_idx] = list(events)
session.insert_events = MagicMock(side_effect=_insert_events)
return session


Expand Down
30 changes: 30 additions & 0 deletions tests/events/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def test_default_fields(self):
assert event.tag is None
assert event.filter_key is None
assert event.object is None
assert event.model_flags > 0
assert event.is_model_visible() is True
assert event.is_summary_event() is False

def test_auto_generated_id_is_valid_uuid(self):
event = Event(invocation_id="inv-1", author="a")
Expand Down Expand Up @@ -308,6 +311,33 @@ def test_error_message_without_code_not_error(self):
assert event.is_error() is False


# ---------------------------------------------------------------------------
# Event model visibility / summary flags
# ---------------------------------------------------------------------------


class TestEventModelFlags:
def test_set_model_visible_false(self):
event = Event(invocation_id="inv-1", author="a")
event.set_model_visible(False)
assert event.is_model_visible() is False

def test_visible_field_does_not_control_model_visibility(self):
event = Event(invocation_id="inv-1", author="a", visible=False)
assert event.is_model_visible() is True

def test_set_summary_event_true(self):
event = Event(invocation_id="inv-1", author="a")
event.set_summary_event(True)
assert event.is_summary_event() is True

def test_clear_summary_event(self):
event = Event(invocation_id="inv-1", author="a")
event.set_summary_event(True)
event.set_summary_event(False)
assert event.is_summary_event() is False


# ---------------------------------------------------------------------------
# Event.has_trailing_code_execution_result
# ---------------------------------------------------------------------------
Expand Down
32 changes: 19 additions & 13 deletions tests/server/a2a/converters/test_event_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,25 @@
from unittest.mock import MagicMock, patch

import pytest
from a2a.types import (
Artifact,
DataPart,
Message,
Part as A2APart,
Role,
Task,
TaskArtifactUpdateEvent,
TaskState,
TaskStatus,
TaskStatusUpdateEvent,
TextPart,
)
try:
from a2a.types import (
Artifact,
DataPart,
Message,
Part as A2APart,
Role,
Task,
TaskArtifactUpdateEvent,
TaskState,
TaskStatus,
TaskStatusUpdateEvent,
TextPart,
)
except ImportError:
pytest.skip(
"Installed a2a.types does not export DataPart/TextPart; skip legacy A2A tests.",
allow_module_level=True,
)
from google.genai import types as genai_types

from trpc_agent_sdk.context import InvocationContext
Expand Down
10 changes: 9 additions & 1 deletion tests/server/a2a/converters/test_part_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
from unittest.mock import MagicMock

import pytest
from a2a import types as a2a_types
try:
from a2a import types as a2a_types
_ = a2a_types.DataPart
_ = a2a_types.TextPart
except (ImportError, AttributeError):
pytest.skip(
"Installed a2a.types does not export DataPart/TextPart; skip legacy A2A tests.",
allow_module_level=True,
)
from google.genai import types as genai_types

from trpc_agent_sdk.models import TOOL_STREAMING_ARGS
Expand Down
8 changes: 7 additions & 1 deletion tests/server/a2a/converters/test_request_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@

import pytest
from a2a.server.agent_execution.context import RequestContext
from a2a.types import Message, Part, Role, TextPart
try:
from a2a.types import Message, Part, Role, TextPart
except ImportError:
pytest.skip(
"Installed a2a.types does not export TextPart; skip legacy A2A tests.",
allow_module_level=True,
)

from trpc_agent_sdk.server.a2a.converters._request_converter import (
_get_user_id_default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def test_env_var_path(self, mock_set_config, tmp_path, monkeypatch):
@patch("trpc_agent_sdk.server.openclaw.config._config.DEFAULT_CONFIG_PATH")
def test_default_path_fallback(self, mock_default_path, mock_default_dir, mock_set_config, tmp_path, monkeypatch):
monkeypatch.delenv(TRPC_CLAW_CONFIG, raising=False)
default_dir = tmp_path / ".trpc_agent_claw"
default_dir = tmp_path / ".trpc_claw"
default_dir.mkdir()
mock_default_dir.exists.return_value = True
cfg_file = default_dir / "config.yaml"
Expand Down
File renamed without changes.
13 changes: 9 additions & 4 deletions tests/sessions/test_base_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ def test_filter_by_num_recent_events(self):
for i in range(10):
session.events.append(_make_event(text=f"msg{i}"))
svc.filter_events(session)
assert len(session.events) == 3
assert len(session.events) == 10
visible_events = [event for event in session.events if event.is_model_visible()]
assert [event.get_text() for event in visible_events] == ["msg7", "msg8", "msg9"]

def test_filter_by_event_ttl(self):
config = SessionServiceConfig(event_ttl_seconds=5.0)
Expand All @@ -188,8 +190,10 @@ def test_filter_by_event_ttl(self):
session.events.append(new_event)

svc.filter_events(session)
assert len(session.events) == 1
assert session.events[0].get_text() == "new"
assert len(session.events) == 2
visible_events = [event for event in session.events if event.is_model_visible()]
assert len(visible_events) == 1
assert visible_events[0].get_text() == "new"

def test_filter_no_config(self):
svc = ConcreteSessionService()
Expand All @@ -208,7 +212,8 @@ def test_filter_ttl_removes_all_old(self):
e.timestamp = time.time() - 100
session.events.append(e)
svc.filter_events(session)
assert len(session.events) == 0
assert len(session.events) == 5
assert all(not event.is_model_visible() for event in session.events)


class TestBaseSessionServiceSetSummarizerManager:
Expand Down
Loading
Loading