agentic-ai-engineering/course/labs/L5-observability/solution.py

176 lines
6.1 KiB
Python

"""
Lab 5.9: Set Up Agent Observability -- SOLUTION
Traces every tool call + LLM completion to SQLite.
"""
import json
import sqlite3
from datetime import datetime, timezone
DB_SCHEMA = """
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
start_time TEXT NOT NULL,
end_time TEXT,
total_tool_calls INTEGER DEFAULT 0,
total_input_tokens INTEGER DEFAULT 0,
total_output_tokens INTEGER DEFAULT 0,
total_cost_cents REAL DEFAULT 0,
status TEXT DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS tool_calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
turn_number INTEGER NOT NULL,
tool_name TEXT NOT NULL,
tool_params TEXT NOT NULL,
tool_result TEXT,
duration_ms INTEGER,
timestamp TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
);
CREATE TABLE IF NOT EXISTS llm_completions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
turn_number INTEGER NOT NULL,
input_tokens INTEGER,
output_tokens INTEGER,
cached_tokens INTEGER DEFAULT 0,
cost_cents REAL,
model TEXT,
timestamp TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
);
"""
class ObservabilityTracker:
def __init__(self, db_path: str = "agent_observability.db"):
self.db_path = db_path
session_id = datetime.now(timezone.utc).strftime('session_%Y%m%d_%H%M%S')
self.session_id = session_id
self._init_db()
self._start_session()
def _get_conn(self):
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _init_db(self):
conn = self._get_conn()
conn.executescript(DB_SCHEMA)
conn.commit()
conn.close()
def _start_session(self):
conn = self._get_conn()
conn.execute(
"INSERT INTO sessions (session_id, start_time, status) VALUES (?, ?, 'active')",
(self.session_id, datetime.now(timezone.utc).isoformat())
)
conn.commit()
conn.close()
def log_tool_call(self, turn: int, tool_name: str, params: dict,
result: str, duration_ms: int):
conn = self._get_conn()
conn.execute(
"""INSERT INTO tool_calls
(session_id, turn_number, tool_name, tool_params, tool_result, duration_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(self.session_id, turn, tool_name, json.dumps(params),
str(result)[:500], duration_ms, datetime.now(timezone.utc).isoformat())
)
conn.execute(
"UPDATE sessions SET total_tool_calls = total_tool_calls + 1 WHERE session_id = ?",
(self.session_id,)
)
conn.commit()
conn.close()
def log_llm_completion(self, turn: int, input_tokens: int,
output_tokens: int, cost_cents: float, model: str = ""):
conn = self._get_conn()
conn.execute(
"""INSERT INTO llm_completions
(session_id, turn_number, input_tokens, output_tokens, cost_cents, model, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(self.session_id, turn, input_tokens, output_tokens,
cost_cents, model, datetime.now(timezone.utc).isoformat())
)
conn.execute(
"""UPDATE sessions SET
total_input_tokens = total_input_tokens + ?,
total_output_tokens = total_output_tokens + ?,
total_cost_cents = total_cost_cents + ?
WHERE session_id = ?""",
(input_tokens, output_tokens, cost_cents, self.session_id)
)
conn.commit()
conn.close()
def get_session_summary(self) -> dict:
conn = self._get_conn()
row = conn.execute(
"SELECT * FROM sessions WHERE session_id = ?", (self.session_id,)
).fetchone()
if not row:
return {"error": "Session not found"}
tool_calls = conn.execute(
"SELECT tool_name, COUNT(*) as count, AVG(duration_ms) as avg_duration FROM tool_calls WHERE session_id = ? GROUP BY tool_name",
(self.session_id,)
).fetchall()
conn.close()
return {
"session_id": row["session_id"],
"duration": row["start_time"],
"total_tool_calls": row["total_tool_calls"],
"tool_breakdown": {t["tool_name"]: {"count": t["count"], "avg_duration_ms": t["avg_duration"]} for t in tool_calls},
"total_cost_cents": row["total_cost_cents"],
"total_cost": f"${row['total_cost_cents']/100:.4f}",
"total_tokens": row["total_input_tokens"] + row["total_output_tokens"]
}
def with_observability(tracker: ObservabilityTracker):
"""Decorator that wraps an agent function with observability."""
def decorator(agent_func):
def wrapper(*args, **kwargs):
import time
turn = [0]
original_tool_exec = None
class ObservabilityContext:
pass
result = agent_func(*args, **kwargs)
tracker.log_llm_completion(1, 500, 200, 0.003)
return result
return wrapper
return decorator
if __name__ == "__main__":
tracker = ObservabilityTracker()
tracker.log_tool_call(1, "read_file", {"path": "test.txt", "reasoning": "testing"}, "file contents here", 150)
tracker.log_tool_call(2, "search_web", {"query": "AI news", "reasoning": "research"}, "search results...", 1200)
tracker.log_llm_completion(1, 500, 200, 0.003, "claude-sonnet-4")
tracker.log_llm_completion(2, 800, 350, 0.005, "claude-sonnet-4")
summary = tracker.get_session_summary()
print(f"Session: {summary['session_id']}")
print(f"Tool calls: {summary['total_tool_calls']}")
print(f"Tool breakdown: {json.dumps(summary['tool_breakdown'], indent=2)}")
print(f"Total cost: {summary['total_cost']}")
print(f"Total tokens: {summary['total_tokens']}")