dev_sandbox/.pi/agent/extensions/insights/analyze.py
2026-06-18 14:52:14 -04:00

222 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""Deterministic session analytics for the /insights command.
Walks ~/.pi/agent/sessions, computes usage stats over a window of days,
and prints a JSON summary on stdout. No LLM involved here.
"""
import sys
import os
import json
import glob
import collections
from datetime import datetime, timezone, timedelta
SESSIONS_DIR = os.path.expanduser("~/.pi/agent/sessions")
def parse_ts(s):
if not s:
return None
try:
return datetime.fromisoformat(s.replace("Z", "+00:00"))
except Exception:
return None
def project_name(cwd):
if not cwd:
return "(unknown)"
return os.path.basename(cwd.rstrip("/")) or cwd
def analyze(days=30):
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
files = glob.glob(os.path.join(SESSIONS_DIR, "*", "*.jsonl"))
sessions = []
tool_counts = collections.Counter()
model_counts = collections.Counter()
project_stats = collections.defaultdict(lambda: {
"sessions": 0, "user_msgs": 0, "assistant_msgs": 0,
"tool_calls": 0, "cost": 0.0, "tokens": 0,
})
hour_hist = collections.Counter()
dow_hist = collections.Counter()
day_hist = collections.Counter()
totals = {
"sessions": 0, "user_msgs": 0, "assistant_msgs": 0, "tool_results": 0,
"tool_calls": 0, "cost": 0.0, "input_tokens": 0, "output_tokens": 0,
"cache_read": 0, "cache_write": 0, "total_tokens": 0,
"compactions": 0, "branches": 0,
}
for fpath in files:
header = None
first_ts = None
last_ts = None
s_user = s_asst = s_tools = s_toolres = 0
s_cost = 0.0
s_tokens = 0
s_compactions = 0
s_branches = 0
s_models = set()
s_tool_counter = collections.Counter()
cwd = None
try:
with open(fpath, "r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
try:
e = json.loads(line)
except Exception:
continue
et = e.get("type")
if et == "session":
header = e
cwd = e.get("cwd")
first_ts = parse_ts(e.get("timestamp"))
continue
ts = parse_ts(e.get("timestamp"))
if ts:
if first_ts is None:
first_ts = ts
last_ts = ts
if et == "compaction":
s_compactions += 1
elif et == "branch_summary":
s_branches += 1
elif et == "message":
msg = e.get("message", {})
role = msg.get("role")
if role == "user":
s_user += 1
elif role == "assistant":
s_asst += 1
if msg.get("model"):
s_models.add(msg["model"])
u = msg.get("usage") or {}
s_cost += (u.get("cost") or {}).get("total", 0) or 0
s_tokens += u.get("totalTokens", 0) or 0
totals["input_tokens"] += u.get("input", 0) or 0
totals["output_tokens"] += u.get("output", 0) or 0
totals["cache_read"] += u.get("cacheRead", 0) or 0
totals["cache_write"] += u.get("cacheWrite", 0) or 0
for c in msg.get("content", []):
if isinstance(c, dict) and c.get("type") == "toolCall":
name = c.get("name", "?")
s_tools += 1
s_tool_counter[name] += 1
elif role == "toolResult":
s_toolres += 1
except Exception:
continue
# Window filter: use session start time
anchor = first_ts or last_ts
if anchor is None or anchor < cutoff:
continue
proj = project_name(cwd)
ps = project_stats[proj]
ps["sessions"] += 1
ps["user_msgs"] += s_user
ps["assistant_msgs"] += s_asst
ps["tool_calls"] += s_tools
ps["cost"] += s_cost
ps["tokens"] += s_tokens
for name, n in s_tool_counter.items():
tool_counts[name] += n
for m in s_models:
model_counts[m] += 1
if anchor:
local = anchor.astimezone()
hour_hist[local.hour] += 1
dow_hist[local.weekday()] += 1
day_hist[local.strftime("%Y-%m-%d")] += 1
duration_min = None
if first_ts and last_ts and last_ts > first_ts:
duration_min = round((last_ts - first_ts).total_seconds() / 60.0, 1)
sessions.append({
"file": os.path.basename(fpath),
"project": proj,
"cwd": cwd,
"start": anchor.isoformat() if anchor else None,
"duration_min": duration_min,
"user_msgs": s_user,
"assistant_msgs": s_asst,
"tool_calls": s_tools,
"tool_results": s_toolres,
"cost": round(s_cost, 4),
"tokens": s_tokens,
"compactions": s_compactions,
"branches": s_branches,
"models": sorted(s_models),
})
totals["sessions"] += 1
totals["user_msgs"] += s_user
totals["assistant_msgs"] += s_asst
totals["tool_results"] += s_toolres
totals["tool_calls"] += s_tools
totals["cost"] += s_cost
totals["total_tokens"] += s_tokens
totals["compactions"] += s_compactions
totals["branches"] += s_branches
totals["cost"] = round(totals["cost"], 4)
projects = []
for name, ps in project_stats.items():
projects.append({
"project": name,
"sessions": ps["sessions"],
"user_msgs": ps["user_msgs"],
"assistant_msgs": ps["assistant_msgs"],
"tool_calls": ps["tool_calls"],
"cost": round(ps["cost"], 4),
"tokens": ps["tokens"],
})
projects.sort(key=lambda p: p["sessions"], reverse=True)
durations = [s["duration_min"] for s in sessions if s["duration_min"]]
avg_duration = round(sum(durations) / len(durations), 1) if durations else None
active_days = len(day_hist)
return {
"window_days": days,
"generated_at": datetime.now(timezone.utc).isoformat(),
"totals": totals,
"active_days": active_days,
"avg_session_duration_min": avg_duration,
"projects": projects,
"tool_usage": [{"name": k, "count": v} for k, v in tool_counts.most_common()],
"model_usage": [{"model": k, "sessions": v} for k, v in model_counts.most_common()],
"hour_histogram": [hour_hist.get(h, 0) for h in range(24)],
"dow_histogram": [dow_hist.get(d, 0) for d in range(7)],
"daily_histogram": dict(sorted(day_hist.items())),
"longest_sessions": sorted(
[s for s in sessions if s["duration_min"]],
key=lambda s: s["duration_min"], reverse=True
)[:10],
"costliest_sessions": sorted(sessions, key=lambda s: s["cost"], reverse=True)[:10],
}
if __name__ == "__main__":
days = 30
if len(sys.argv) > 1:
try:
days = int(sys.argv[1])
except ValueError:
pass
print(json.dumps(analyze(days), indent=2))