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.
- agent_coach_cli-0.1.0.dist-info/METADATA +242 -0
- agent_coach_cli-0.1.0.dist-info/RECORD +19 -0
- agent_coach_cli-0.1.0.dist-info/WHEEL +5 -0
- agent_coach_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agent_coach_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- agent_coach_cli-0.1.0.dist-info/top_level.txt +1 -0
- agentcoach/__init__.py +1 -0
- agentcoach/__main__.py +3 -0
- agentcoach/cli.py +254 -0
- agentcoach/collect.py +336 -0
- agentcoach/heuristics.py +134 -0
- agentcoach/i18n.py +88 -0
- agentcoach/judge.py +328 -0
- agentcoach/paths.py +24 -0
- agentcoach/redact.py +52 -0
- agentcoach/report.py +559 -0
- agentcoach/rubric/default.json +561 -0
- agentcoach/rubric.py +46 -0
- agentcoach/store.py +102 -0
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
|