agent-coach-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agentcoach/store.py ADDED
@@ -0,0 +1,102 @@
1
+ """Run records: one JSON file per scoring run under ~/.agent-coach/history/.
2
+
3
+ Transcripts get deleted by the tools themselves (Claude Code keeps 30 days by
4
+ default), so each run stores everything the report needs: scores, notes and
5
+ the short coaching excerpts. Nothing here is ever uploaded.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+
13
+ from .collect import Session
14
+ from .paths import history_dir
15
+ from .redact import redact
16
+
17
+
18
+ def aggregate(results: list[dict], rubric: dict) -> dict:
19
+ out = {}
20
+ for d in rubric["dimensions"]:
21
+ scores = [r["dimensions"][d["id"]]["score"] for r in results
22
+ if r.get("dimensions", {}).get(d["id"], {}).get("score") is not None]
23
+ out[d["id"]] = {
24
+ "score": round(sum(scores) / (2 * len(scores)) * 100) if scores else None,
25
+ "n": len(scores),
26
+ "missed": sum(1 for x in scores if x == 0),
27
+ }
28
+ vals = [v["score"] for v in out.values() if v["score"] is not None]
29
+ return {"overall": round(sum(vals) / len(vals)) if vals else None, "dimensions": out}
30
+
31
+
32
+ def miss_summary(sessions: list[dict]) -> dict:
33
+ """How often the user didn't get what they wanted, and how much of that was their own prompt gap."""
34
+ by_cause: dict[str, int] = {}
35
+ turns = 0
36
+ for s in sessions:
37
+ turns += s.get("turns", 0)
38
+ for m in s.get("misses", []):
39
+ by_cause[m["cause"]] = by_cause.get(m["cause"], 0) + 1
40
+ total = sum(by_cause.values())
41
+ gaps = by_cause.get("prompt_gap", 0)
42
+ return {"total": total, "by_cause": by_cause, "turns": turns,
43
+ "gap_share": round(gaps / total * 100) if total else None,
44
+ "gaps_per_100_turns": round(gaps / turns * 100, 1) if turns else None,
45
+ "themes": []}
46
+
47
+
48
+ def build_run(sessions: list[Session], results: dict[str, dict], rubric: dict,
49
+ window_start: str | None, judge: str) -> dict:
50
+ per = []
51
+ for s in sessions:
52
+ r = results.get(s.id)
53
+ if not r or "dimensions" not in r:
54
+ continue
55
+ turns = {t.index: t for t in s.turns}
56
+ dims = r["dimensions"]
57
+ for v in dims.values():
58
+ t = turns.get(v.get("evidence_turn") or -1)
59
+ v["excerpt"] = redact(t.text[:220]) if t else ""
60
+ per.append({
61
+ "id": s.id, "source": s.source, "project": s.project,
62
+ "started": s.started, "ended": s.ended, "turns": len(s.turns),
63
+ "task_summary": redact(r.get("task_summary", "")),
64
+ "judge": r.get("judge", judge),
65
+ "dimensions": dims,
66
+ "coaching": [{**c, "original": redact(c.get("original", "")),
67
+ "better": redact(c.get("better", ""))} for c in r.get("coaching", [])],
68
+ "misses": [{**m, **{k: redact(m.get(k, "")) for k in ("original", "better", "what_went_wrong", "why")}}
69
+ for m in r.get("misses", [])],
70
+ })
71
+ agg = aggregate(per, rubric)
72
+ agg["misses"] = miss_summary(per)
73
+ now = datetime.now().astimezone()
74
+ return {
75
+ "schema": 1,
76
+ "created": now.isoformat(timespec="seconds"),
77
+ "window_start": window_start,
78
+ "judge": judge,
79
+ "rubric": {"id": rubric["id"], "version": rubric.get("version", 1)},
80
+ "sources": sorted({p["source"] for p in per}),
81
+ "session_count": len(per),
82
+ **agg,
83
+ "sessions": per,
84
+ }
85
+
86
+
87
+ def save_run(run: dict) -> Path:
88
+ stamp = run["created"].replace(":", "").replace("-", "")[:15]
89
+ path = history_dir() / f"run-{stamp}.json"
90
+ path.write_text(json.dumps(run, ensure_ascii=False, indent=1), encoding="utf-8")
91
+ return path
92
+
93
+
94
+ def load_runs() -> list[dict]:
95
+ runs = []
96
+ for p in sorted(history_dir().glob("run-*.json")):
97
+ try:
98
+ runs.append(json.loads(p.read_text(encoding="utf-8")))
99
+ except (OSError, json.JSONDecodeError):
100
+ continue
101
+ runs.sort(key=lambda r: r["created"])
102
+ return runs