Skip to content

Commit 2fad8e6

Browse files
committed
add module 9 files
1 parent 9265033 commit 2fad8e6

149 files changed

Lines changed: 21077 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
OPENAI_API_KEY=sk-...
2+
3+
# Langfuse tracing — see LANGFUSE.md to start the local stack (http://localhost:3000).
4+
LANGFUSE_PUBLIC_KEY=pk-lf-...
5+
LANGFUSE_SECRET_KEY=sk-lf-...
6+
LANGFUSE_HOST=http://localhost:3000

module-09-tracing/demos/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/state.db
2+
/last_run_id.txt
3+
/logs/runs.jsonl
4+
.venv
5+
__pycache__/
6+
.env
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Local Langfuse for the tracing demo
2+
3+
`demo_02.py` ships its span tree to Langfuse. Run Langfuse locally with its official
4+
Docker stack, pinned to a release:
5+
6+
```bash
7+
git clone --branch v3.130.0 https://github.com/langfuse/langfuse.git
8+
cd langfuse
9+
docker compose up -d # UI on http://localhost:3000 after ~2-3 min
10+
```
11+
12+
Then:
13+
14+
1. Open `http://localhost:3000`, create an account (local, not the cloud), create a project.
15+
2. Copy the project's **public** and **secret** keys into this demo's `.env`:
16+
17+
```
18+
LANGFUSE_PUBLIC_KEY=pk-lf-...
19+
LANGFUSE_SECRET_KEY=sk-lf-...
20+
LANGFUSE_HOST=http://localhost:3000
21+
```
22+
23+
3. Run the traced demo and open the run in the UI:
24+
25+
```bash
26+
uv run python demo_02.py --case traced-run
27+
```
28+
29+
This is a **separate** Docker stack from any RealThor container — run it from the cloned
30+
`langfuse/` directory with its own `docker compose`. Pin the release tag; the self-hosting
31+
stack evolves. Current quickstart: https://langfuse.com/self-hosting/deployment/docker-compose
32+
33+
The backend is chosen entirely by env vars, so switching to Langfuse Cloud is just a `.env`
34+
change: set `LANGFUSE_HOST=https://us.cloud.langfuse.com` (or the EU host) and use the keys
35+
from your cloud project.
36+
37+
## What to look at once it's open
38+
39+
After `demo_02.py` runs, go to **Tracing → Traces** and filter by the `north-ibuyer` tag.
40+
Open the trace and read the span tree:
41+
42+
- **Spans** = tool calls (`get_demand_score`, `get_supply_snapshot`) — click one to see its
43+
input arguments and the row it returned.
44+
- **Generations** = LLM calls — these also show the model, token counts, cost, and latency.
45+
The final generation is where the model turned the tool results into its recommendation.
46+
47+
Top to bottom, the tree is the reasoning path: what the agent fetched, in what order, and
48+
how it decided.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
agent:
2+
name: RealThor
3+
version: "0.9.0"
4+
5+
model:
6+
name: gpt-4o-mini
7+
# Non-zero: the recommendation can vary run to run — the non-determinism that makes a
8+
# single run worth tracing rather than just re-running.
9+
temperature: 0.7
10+
11+
debug:
12+
state_db: state.db
13+
14+
tracing:
15+
enabled: true
16+
17+
logging:
18+
log_dir: logs
19+
20+
paths:
21+
prompt: prompts/system_prompt_v9.md
22+
demand_data: data/demand_signals.csv
23+
supply_data: data/supply_snapshot.csv
24+
velocity_data: data/listing_velocity.csv
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
region,property_type,bedrooms,search_volume,lead_count,recent_sales,demand_score
2+
west,apartment,1,890,64,24,0.77
3+
west,apartment,2,1240,86,34,0.91
4+
west,house,3,520,38,14,0.63
5+
west,studio,1,720,51,19,0.69
6+
north,apartment,1,620,31,12,0.54
7+
north,apartment,2,940,71,26,0.80
8+
north,house,2,310,22,8,0.41
9+
north,house,3,280,19,6,0.37
10+
south,apartment,2,780,58,19,0.72
11+
south,house,3,710,42,18,0.68
12+
south,house,4,390,27,9,0.49
13+
south,studio,1,810,58,22,0.78
14+
east,apartment,1,650,47,17,0.66
15+
east,apartment,2,980,73,21,0.82
16+
east,studio,1,570,35,13,0.59
17+
east,house,3,460,33,11,0.55
18+
center,studio,1,430,19,8,0.47
19+
center,apartment,1,680,45,16,0.65
20+
center,apartment,2,510,37,12,0.56
21+
center,house,2,490,33,11,0.48
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
region,property_type,bedrooms,avg_days_to_offer,avg_days_to_close,velocity_score,data_confidence
2+
west,apartment,1,8,21,0.82,high
3+
west,apartment,2,5,18,0.91,high
4+
west,house,3,19,34,0.63,medium
5+
west,studio,1,11,24,0.71,medium
6+
north,apartment,1,22,41,0.52,medium
7+
north,apartment,2,12,29,0.78,high
8+
north,house,2,38,58,0.39,low
9+
north,house,3,44,63,0.34,low
10+
south,apartment,2,16,32,0.68,medium
11+
south,house,3,18,35,0.65,medium
12+
south,house,4,29,47,0.48,medium
13+
south,studio,1,9,22,0.8,high
14+
east,apartment,1,21,38,0.62,medium
15+
east,apartment,2,13,27,0.77,high
16+
east,studio,1,31,51,0.55,low
17+
east,house,3,24,42,0.58,medium
18+
center,studio,1,44,64,0.43,low
19+
center,apartment,1,26,45,0.61,medium
20+
center,apartment,2,33,53,0.52,medium
21+
center,house,2,37,57,0.46,low
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
region,property_type,bedrooms,active_listings,avg_days_on_market,supply_score
2+
west,apartment,1,42,31,0.44
3+
west,apartment,2,18,21,0.22
4+
west,house,3,67,48,0.72
5+
west,studio,1,48,36,0.52
6+
north,apartment,1,77,54,0.81
7+
north,apartment,2,24,28,0.31
8+
north,house,2,88,62,0.88
9+
north,house,3,94,68,0.91
10+
south,apartment,2,39,35,0.48
11+
south,house,3,36,39,0.55
12+
south,house,4,52,44,0.62
13+
south,studio,1,22,19,0.27
14+
east,apartment,1,53,43,0.51
15+
east,apartment,2,29,28,0.37
16+
east,studio,1,83,57,0.85
17+
east,house,3,44,41,0.58
18+
center,studio,1,91,63,0.88
19+
center,apartment,1,58,49,0.68
20+
center,apartment,2,71,52,0.76
21+
center,house,2,64,51,0.67

module-09-tracing/demos/demo_01.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Demo 1 — Logs and state aren't enough.
2+
3+
RealThor already logs every run and can replay one from persisted state (M07-M08). Run
4+
the North iBuyer question, then look at what those two artifacts actually show: the log
5+
line says what happened and how it ended; the replay restores the tool trajectory and the
6+
values each tool returned. Neither records *why* the model weighted margin over demand —
7+
the reasoning path between the tool results and the decision. That gap is what tracing
8+
closes in demo_02.
9+
10+
uv run python demo_01.py --case all
11+
"""
12+
13+
import json
14+
import uuid
15+
from datetime import datetime, timezone
16+
from pathlib import Path
17+
18+
import click
19+
from dotenv import load_dotenv
20+
from langchain_core.messages import HumanMessage, ToolMessage
21+
22+
from realthor.agents import build_agent
23+
from realthor.config import load_config, resolve_paths
24+
from realthor.replay import build_checkpointer, format_report, reconstruct_context
25+
26+
load_dotenv()
27+
28+
QUESTION = "Should we acquire through iBuyer in north for 3-bedroom houses?"
29+
30+
31+
def _log(cfg, run_id, result):
32+
"""Write the kind of structured log line the monitoring system records per run."""
33+
tools = [m.name for m in result["messages"] if isinstance(m, ToolMessage) and m.name]
34+
log_dir = Path(cfg["logging"]["log_dir"])
35+
log_dir.mkdir(parents=True, exist_ok=True)
36+
row = {
37+
"run_id": run_id,
38+
"timestamp": datetime.now(timezone.utc).isoformat(),
39+
"input_text": QUESTION,
40+
"success": True,
41+
"tools_called": tools,
42+
"prompt_version": Path(cfg["paths"]["prompt"]).name,
43+
"dataset_version": cfg["agent"]["version"],
44+
}
45+
with open(log_dir / "runs.jsonl", "a") as f:
46+
f.write(json.dumps(row) + "\n")
47+
return row
48+
49+
50+
@click.command()
51+
@click.option("--case", default="all", type=click.Choice(["log", "replay", "all"]))
52+
def main(case):
53+
cfg = resolve_paths(load_config("config.yaml"), "config.yaml")
54+
log_path = Path(cfg["logging"]["log_dir"]) / "runs.jsonl"
55+
state_path = Path("last_run_id.txt")
56+
57+
if case in ("log", "all"):
58+
run_id = str(uuid.uuid4())
59+
agent = build_agent(cfg, checkpointer=build_checkpointer(cfg))
60+
result = agent.invoke(
61+
{"messages": [HumanMessage(QUESTION)]},
62+
config={"configurable": {"thread_id": run_id}},
63+
)
64+
row = _log(cfg, run_id, result)
65+
state_path.write_text(run_id)
66+
click.echo("=== The log line (logs/runs.jsonl) ===")
67+
click.echo(json.dumps(row, indent=2))
68+
click.echo("\nThe log says a run happened, which tools it called, and that it succeeded.")
69+
70+
if case in ("replay", "all"):
71+
run_id = state_path.read_text().strip() if state_path.exists() else None
72+
if not run_id:
73+
click.echo("No run to replay — run `--case log` first.")
74+
return
75+
click.echo("\n=== The replay (reconstructed from state.db) ===")
76+
ctx = reconstruct_context(run_id, build_checkpointer(cfg), log_path)
77+
click.echo(format_report(ctx))
78+
79+
if case == "all":
80+
click.echo("\n--- The gap ---")
81+
click.echo("The log shows what happened. The replay restores the state and the values.")
82+
click.echo("Neither shows HOW the model weighed those values into the recommendation —")
83+
click.echo("the reasoning path between the tool results and the decision. That is what a")
84+
click.echo("trace records.")
85+
86+
87+
if __name__ == "__main__":
88+
main()

module-09-tracing/demos/demo_02.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Demo 2 — The trace.
2+
3+
Same North iBuyer question, now with a Langfuse CallbackHandler attached to the invoke.
4+
The handler records a span tree — one span per model call and tool call — and ships it to
5+
Langfuse. Open the run in the UI (http://localhost:3000 for the local stack) and the
6+
reasoning path is finally visible: the sequential tool spans get_demand_score →
7+
get_supply_snapshot → recommend_channel, each with the values it returned, showing the low
8+
demand score was fetched but not weighted in the decision.
9+
10+
Requires a Langfuse backend and LANGFUSE_* keys in .env (see LANGFUSE.md).
11+
12+
uv run python demo_02.py --case traced-run
13+
"""
14+
15+
import uuid
16+
17+
import click
18+
from dotenv import load_dotenv
19+
from langchain_core.messages import HumanMessage
20+
21+
from realthor.agents import build_agent
22+
from realthor.config import load_config, resolve_paths
23+
from realthor.replay import build_checkpointer
24+
from realthor.tracing import build_tracer, flush_tracer
25+
26+
load_dotenv()
27+
28+
QUESTION = "Should we acquire through iBuyer in north for 3-bedroom houses?"
29+
30+
31+
@click.command()
32+
@click.option("--case", default="traced-run", type=click.Choice(["traced-run"]))
33+
def main(case):
34+
cfg = resolve_paths(load_config("config.yaml"), "config.yaml")
35+
36+
tracer = build_tracer(cfg)
37+
if tracer is None:
38+
click.echo("Tracing is off or unconfigured.")
39+
click.echo("Set tracing.enabled: true and put LANGFUSE_* keys in .env (see LANGFUSE.md).")
40+
return
41+
42+
run_id = str(uuid.uuid4())
43+
agent = build_agent(cfg, checkpointer=build_checkpointer(cfg))
44+
try:
45+
result = agent.invoke(
46+
{"messages": [HumanMessage(QUESTION)]},
47+
config={
48+
"configurable": {"thread_id": run_id},
49+
"callbacks": [tracer],
50+
# Tags make this run easy to find and filter in the Langfuse UI.
51+
"metadata": {"langfuse_tags": ["demo", "north-ibuyer"]},
52+
},
53+
)
54+
click.echo("=== Traced run ===")
55+
click.echo(result["messages"][-1].content)
56+
finally:
57+
# Flush before the process exits, or the batched spans never reach Langfuse.
58+
flush_tracer(tracer)
59+
60+
click.echo(f"\nRun ID: {run_id}")
61+
click.echo("Trace sent to Langfuse — open http://localhost:3000 and inspect the span tree.")
62+
63+
64+
if __name__ == "__main__":
65+
main()

0 commit comments

Comments
 (0)