Skip to content

Commit 1bfab00

Browse files
committed
fix(examples): fix broken API usage and add streaming/persistence examples
Phase 1 — Bug fixes: - 01_basic_usage: unpack run() tuple, replace phantom total_* properties with _usage_metrics pattern, update llm from gpt-4 to claude-sonnet-4-20250514 - 02_custom_tools: rewrite add_tool() calls to pass callable only; schema is auto-derived from docstring and type hints - 06_tool_retrieval: rename enable_retrieval -> use_tool_retriever; fix run() unpacking (×2) - 04_resource_management: fix run() unpacking - 13_multi_agent: add MaxRoundsExceededError handling (raised by supervisor) Phase 3 — New examples: - 15_streaming: run_stream() with EventType filtering and JSON serialisation - 16_persistence: checkpoint_db_path + thread_id multi-turn conversation Phase 3C — Tests: - test_examples_streaming_persistence: 9 unit tests covering streaming filter, JSON serialisation, thread_id reuse, and persistence setup Phase 4 — Docs: - examples/README: add entries for all 16 examples (7–10, 12–13, 15–16 were previously undocumented); note duplicate 10_ prefix; flag mock-based examples 9/10 as no-API-key needed
1 parent 2507375 commit 1bfab00

9 files changed

Lines changed: 390 additions & 65 deletions
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Tests for the patterns demonstrated in examples 15 (streaming) and 16 (persistence).
2+
3+
These tests use mock LLMs so no API key is required.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import asyncio
9+
import uuid
10+
from unittest.mock import MagicMock, patch
11+
12+
import pytest
13+
from langchain_core.messages import AIMessage, HumanMessage
14+
15+
from BaseAgent.events import AgentEvent, EventType
16+
17+
18+
# ---------------------------------------------------------------------------
19+
# Helpers
20+
# ---------------------------------------------------------------------------
21+
22+
23+
def _make_agent(checkpoint_db_path: str = ":memory:"):
24+
mock_llm = MagicMock()
25+
mock_llm.model_name = "mock-model"
26+
mock_llm.invoke.return_value = MagicMock(content="<solution>done</solution>")
27+
with patch("BaseAgent.base_agent.get_llm", return_value=("Anthropic", mock_llm)):
28+
from BaseAgent.base_agent import BaseAgent
29+
return BaseAgent(checkpoint_db_path=checkpoint_db_path, require_approval="never")
30+
31+
32+
async def _collect(agen) -> list[AgentEvent]:
33+
return [item async for item in agen]
34+
35+
36+
def _make_raw_events(events: list[dict]):
37+
async def _gen():
38+
for e in events:
39+
yield e
40+
return lambda *args, **kwargs: _gen()
41+
42+
43+
# ---------------------------------------------------------------------------
44+
# Streaming (example 15) patterns
45+
# ---------------------------------------------------------------------------
46+
47+
48+
@pytest.mark.unit
49+
class TestStreamingExample:
50+
@pytest.fixture
51+
def agent(self):
52+
return _make_agent()
53+
54+
def _thinking_events(self):
55+
ai_msg = AIMessage(content="<think>reasoning</think>")
56+
state = {"input": [HumanMessage(content="task"), ai_msg], "next_step": "generate"}
57+
return [
58+
{"event": "on_chain_end", "metadata": {"langgraph_node": "generate"}, "data": {"output": state}},
59+
]
60+
61+
def test_stream_all_events_yields_thinking(self, agent):
62+
agent.app.astream_events = _make_raw_events(self._thinking_events())
63+
collected = asyncio.run(_collect(agent.run_stream("task")))
64+
assert len(collected) == 1
65+
assert collected[0].event_type == EventType.THINKING
66+
67+
def test_stream_filter_final_answer_only(self, agent):
68+
ai_msg = AIMessage(content="<solution>answer</solution>")
69+
state = {"input": [HumanMessage(content="task"), ai_msg], "next_step": "end"}
70+
raw = [{"event": "on_chain_end", "metadata": {"langgraph_node": "generate"}, "data": {"output": state}}]
71+
agent.app.astream_events = _make_raw_events(raw)
72+
73+
collected = asyncio.run(
74+
_collect(agent.run_stream("task", event_types={EventType.FINAL_ANSWER}))
75+
)
76+
assert len(collected) == 1
77+
assert collected[0].event_type == EventType.FINAL_ANSWER
78+
79+
def test_stream_filter_excludes_other_types(self, agent):
80+
ai_msg = AIMessage(content="<think>thinking</think>")
81+
state = {"input": [HumanMessage(content="task"), ai_msg], "next_step": "end"}
82+
raw = [{"event": "on_chain_end", "metadata": {"langgraph_node": "generate"}, "data": {"output": state}}]
83+
agent.app.astream_events = _make_raw_events(raw)
84+
85+
collected = asyncio.run(
86+
_collect(agent.run_stream("task", event_types={EventType.FINAL_ANSWER}))
87+
)
88+
assert collected == []
89+
90+
def test_event_to_json_is_serialisable(self, agent):
91+
import json
92+
agent.app.astream_events = _make_raw_events(self._thinking_events())
93+
collected = asyncio.run(_collect(agent.run_stream("task")))
94+
assert collected
95+
payload = collected[0].to_json()
96+
parsed = json.loads(payload)
97+
assert parsed["event_type"] == "thinking"
98+
99+
def test_stream_with_thread_id(self, agent):
100+
agent.app.astream_events = _make_raw_events([])
101+
tid = str(uuid.uuid4())
102+
asyncio.run(_collect(agent.run_stream("task", thread_id=tid)))
103+
assert agent.thread_id == tid
104+
105+
106+
# ---------------------------------------------------------------------------
107+
# Persistence (example 16) patterns
108+
# ---------------------------------------------------------------------------
109+
110+
111+
@pytest.mark.unit
112+
class TestPersistenceExample:
113+
def test_file_checkpoint_path_is_stored(self, tmp_path):
114+
db = str(tmp_path / "conv.db")
115+
agent = _make_agent(checkpoint_db_path=db)
116+
assert agent.checkpoint_db_path == db
117+
118+
def test_same_thread_id_reused_across_runs(self, tmp_path):
119+
db = str(tmp_path / "conv.db")
120+
agent = _make_agent(checkpoint_db_path=db)
121+
122+
mock_resp = MagicMock()
123+
mock_resp.content = "<solution>done</solution>"
124+
agent.llm.invoke.return_value = mock_resp
125+
126+
tid = "my-session"
127+
with patch("BaseAgent.nodes.extract_usage_metrics", return_value=None):
128+
agent.run("first question", thread_id=tid)
129+
assert agent.thread_id == tid
130+
agent.run("follow-up question", thread_id=tid)
131+
assert agent.thread_id == tid
132+
133+
def test_different_thread_ids_produce_separate_histories(self, tmp_path):
134+
db = str(tmp_path / "conv.db")
135+
agent = _make_agent(checkpoint_db_path=db)
136+
137+
mock_resp = MagicMock()
138+
mock_resp.content = "<solution>done</solution>"
139+
agent.llm.invoke.return_value = mock_resp
140+
141+
with patch("BaseAgent.nodes.extract_usage_metrics", return_value=None):
142+
agent.run("question A", thread_id="session-a")
143+
tid_a = agent.thread_id
144+
agent.run("question B", thread_id="session-b")
145+
tid_b = agent.thread_id
146+
147+
assert tid_a == "session-a"
148+
assert tid_b == "session-b"
149+
assert tid_a != tid_b
150+
151+
def test_close_cleans_up_without_error(self, tmp_path):
152+
db = str(tmp_path / "conv.db")
153+
agent = _make_agent(checkpoint_db_path=db)
154+
agent.close() # must not raise

examples/01_basic_usage.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,18 @@
77

88
from BaseAgent import BaseAgent
99

10-
# Initialize the agent with your preferred LLM
11-
agent = BaseAgent(
12-
llm="gpt-4", # or "claude-3-5-sonnet-20241022", "gemini-pro", etc.
13-
path="./data"
14-
)
10+
# Initialize the agent
11+
agent = BaseAgent(llm="claude-sonnet-4-20250514")
1512

16-
# Run a task
17-
result = agent.run("Analyze the dataset and create a visualization")
13+
# run() returns (log, content)
14+
log, result = agent.run("Analyze the dataset and create a visualization")
1815
print(result)
1916

20-
# Access usage metrics
17+
# Access usage metrics — _usage_metrics accumulates across all runs
18+
total_cost = sum(u.cost for u in agent._usage_metrics if u.cost is not None)
19+
total_input = sum(u.input_tokens for u in agent._usage_metrics if u.input_tokens is not None)
20+
total_output = sum(u.output_tokens for u in agent._usage_metrics if u.output_tokens is not None)
2121
print(f"\nUsage Metrics:")
22-
print(f"Input tokens: {agent.total_input_tokens}")
23-
print(f"Output tokens: {agent.total_output_tokens}")
24-
print(f"Total cost: ${agent.total_cost:.4f}")
25-
22+
print(f"Input tokens: {total_input}")
23+
print(f"Output tokens: {total_output}")
24+
print(f"Total cost: ${total_cost:.4f}")

examples/02_custom_tools.py

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
def search_database(query: str, limit: int = 10) -> list:
1212
"""Search the database for relevant entries."""
13-
# Your implementation here
1413
results = [
1514
{"id": 1, "name": "Result 1", "relevance": 0.95},
1615
{"id": 2, "name": "Result 2", "relevance": 0.87},
@@ -28,36 +27,12 @@ def calculate_metrics(data: list, metric_type: str = "mean") -> float:
2827
return 0
2928

3029

31-
# Create agent
3230
agent = BaseAgent()
3331

34-
# Add first custom tool
35-
agent.add_tool(
36-
name="search_database",
37-
function=search_database,
38-
description="Search the database for relevant entries",
39-
required_parameters=[
40-
{"name": "query", "description": "Search query", "type": "str"}
41-
],
42-
optional_parameters=[
43-
{"name": "limit", "description": "Maximum results", "type": "int", "default": 10}
44-
]
45-
)
46-
47-
# Add second custom tool
48-
agent.add_tool(
49-
name="calculate_metrics",
50-
function=calculate_metrics,
51-
description="Calculate statistical metrics from numerical data",
52-
required_parameters=[
53-
{"name": "data", "description": "List of numbers", "type": "list"}
54-
],
55-
optional_parameters=[
56-
{"name": "metric_type", "description": "Type of metric (mean, sum)", "type": "str", "default": "mean"}
57-
]
58-
)
32+
# add_tool() takes a callable; name, description, and schema are derived
33+
# automatically from the function's name, docstring, and type hints.
34+
agent.add_tool(search_database)
35+
agent.add_tool(calculate_metrics)
5936

60-
# Use the agent with your custom tools
61-
result = agent.run("Search for proteins related to cancer and calculate the mean relevance")
37+
log, result = agent.run("Search for proteins related to cancer and calculate the mean relevance")
6238
print(result)
63-

examples/04_resource_management.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
print(f"Libraries: {summary['libraries']['total']}")
7171

7272
# Use the agent with custom resources
73-
result = agent.run("Analyze my_dataset.csv using custom_analysis_lib")
73+
log, result = agent.run("Analyze my_dataset.csv using custom_analysis_lib")
7474
print(f"\n=== Agent Result ===")
7575
print(result)
7676

examples/06_tool_retrieval.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,24 @@
77

88
from BaseAgent import BaseAgent
99

10-
# Enable automatic tool selection/retrieval
10+
# Enable automatic tool selection: the agent embeds and ranks tools
11+
# by relevance before each run instead of passing the full tool list.
1112
agent = BaseAgent(
12-
llm="gpt-4",
13-
enable_retrieval=True # Agent will automatically select relevant tools
13+
llm="claude-sonnet-4-20250514",
14+
use_tool_retriever=True,
1415
)
1516

16-
# The agent will automatically determine which tools are needed
17-
result = agent.run(
17+
log, result = agent.run(
1818
"Analyze protein sequences, calculate binding affinities, "
1919
"and create a visualization of the results"
2020
)
21-
2221
print(result)
2322

24-
# You can also manually select tools
23+
# You can also manually select a subset of tools for a specific task.
2524
agent.resource_manager.select_tools_by_names([
2625
"run_python_repl",
27-
"fetch_data"
26+
"fetch_data",
2827
])
2928

30-
# Only selected tools will be available for this task
31-
result = agent.run("Run a Python analysis on the data")
29+
log, result = agent.run("Run a Python analysis on the data")
3230
print(result)
33-

examples/13_multi_agent.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
python examples/13_multi_agent.py
1010
"""
1111

12-
from BaseAgent import BaseAgent, AgentTeam
12+
from BaseAgent import BaseAgent, AgentTeam, MaxRoundsExceededError
1313
from BaseAgent.agent_spec import AgentSpec
1414

1515

@@ -43,6 +43,8 @@ def two_agent_pipeline():
4343
"Analyse the key risk factors for Alzheimer's disease and write a one-paragraph summary."
4444
)
4545
print(result)
46+
except MaxRoundsExceededError as e:
47+
print(f"Team hit the round limit before finishing: {e}")
4648
finally:
4749
team.close()
4850

@@ -86,6 +88,8 @@ def three_agent_pipeline():
8688
"and write a structured two-paragraph report."
8789
)
8890
print(result)
91+
except MaxRoundsExceededError as e:
92+
print(f"Team hit the round limit before finishing: {e}")
8993
finally:
9094
team.close()
9195

examples/15_streaming.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Example 15: Streaming events with run_stream().
2+
3+
run_stream() is an async generator that yields AgentEvent objects as the agent
4+
executes — one event per reasoning block, code execution, or final answer.
5+
Use it to feed a UI or collect fine-grained execution traces in real time.
6+
7+
Run this script from the repo root::
8+
9+
python examples/15_streaming.py
10+
"""
11+
12+
import asyncio
13+
14+
from BaseAgent import BaseAgent, EventType
15+
16+
17+
async def stream_all_events():
18+
"""Print every event emitted during a run."""
19+
agent = BaseAgent(llm="claude-sonnet-4-20250514", require_approval="never")
20+
21+
print("=== All events ===")
22+
async for event in agent.run_stream("What is the GC content of the sequence ATCGATCG?"):
23+
print(f"[{event.event_type.value:20s}] {event.content[:120]}")
24+
25+
26+
async def stream_filtered_events():
27+
"""Receive only THINKING and FINAL_ANSWER events."""
28+
agent = BaseAgent(llm="claude-sonnet-4-20250514", require_approval="never")
29+
30+
print("\n=== Filtered: THINKING + FINAL_ANSWER only ===")
31+
async for event in agent.run_stream(
32+
"Summarise the role of BRCA1 in DNA repair.",
33+
event_types={EventType.THINKING, EventType.FINAL_ANSWER},
34+
):
35+
label = "Reasoning" if event.event_type == EventType.THINKING else "Answer"
36+
print(f"[{label}] {event.content[:200]}")
37+
38+
39+
async def stream_to_json():
40+
"""Serialise each event to JSON (useful for WebSocket / SSE payloads)."""
41+
agent = BaseAgent(llm="claude-sonnet-4-20250514", require_approval="never")
42+
43+
print("\n=== JSON payloads ===")
44+
async for event in agent.run_stream(
45+
"Name three model organisms used in genetics research.",
46+
event_types={EventType.FINAL_ANSWER},
47+
):
48+
print(event.to_json())
49+
50+
51+
if __name__ == "__main__":
52+
asyncio.run(stream_all_events())
53+
asyncio.run(stream_filtered_events())
54+
asyncio.run(stream_to_json())

0 commit comments

Comments
 (0)