@iseoane/agent-depot 0.2.0
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.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +465 -0
- package/package.json +69 -0
- package/skills/doctor-md-agents/README.md +51 -0
- package/skills/doctor-md-agents/SKILL.md +275 -0
- package/skills/doctor-md-agents/references/rule-model.md +56 -0
- package/skills/doctor-md-agents/references/structure.md +89 -0
- package/skills/doctor-md-agents/scorers/adherence.md +73 -0
- package/skills/doctor-md-agents/scorers/rule-value.md +49 -0
- package/skills/doctor-md-agents/scripts/collect_sessions.py +1135 -0
- package/skills/doctor-md-agents/scripts/extract_rules.py +704 -0
- package/skills/doctor-md-agents/scripts/render_report.py +644 -0
- package/skills/doctor-md-agents/scripts/scan_corrections.py +781 -0
- package/skills/doctor-md-agents/scripts/test_collect_sessions.py +780 -0
- package/skills/doctor-md-agents/scripts/test_extract_rules.py +530 -0
- package/skills/doctor-md-agents/scripts/test_render_report.py +383 -0
- package/skills/doctor-md-agents/scripts/test_scan_corrections.py +343 -0
- package/skills/doctor-md-agents/scripts/test_verify_citations.py +236 -0
- package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +507 -0
- package/skills/doctor-md-agents/scripts/verify_citations.py +217 -0
- package/skills/doctor-md-skill/README.md +27 -0
- package/skills/doctor-md-skill/SKILL.md +212 -0
- package/skills/doctor-md-skill/assets/pierre-diffs.js +1531 -0
- package/skills/doctor-md-skill/references/skill-improvements.md +32 -0
- package/skills/doctor-md-skill/references/supported-harnesses.md +44 -0
- package/skills/doctor-md-skill/scorers/code-quality.md +41 -0
- package/skills/doctor-md-skill/scorers/efficiency.md +38 -0
- package/skills/doctor-md-skill/scripts/collect_sessions.py +1112 -0
- package/skills/doctor-md-skill/scripts/detect_machine_sessions.py +315 -0
- package/skills/doctor-md-skill/scripts/render_report.py +545 -0
- package/skills/doctor-md-skill/scripts/test_collect_sessions.py +593 -0
- package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +181 -0
- package/skills/doctor-md-skill/scripts/test_render_report.py +241 -0
- package/skills/doctor-md-skill/scripts/test_verify_citations.py +196 -0
- package/skills/doctor-md-skill/scripts/verify_citations.py +221 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify that a scorer's cited quote actually appears in the transcript it
|
|
3
|
+
claims to come from -- SKILL.md Step 2's mechanical check on scoring results.
|
|
4
|
+
|
|
5
|
+
Both scorers (scorers/efficiency.md, scorers/code-quality.md) are asked to
|
|
6
|
+
copy `quote` verbatim from the transcript they were shown, alongside the
|
|
7
|
+
existing 1-3 sentence `reason`. This script is the only thing that checks
|
|
8
|
+
that claim: it whitespace-folds both the quote and the transcript text and
|
|
9
|
+
requires the folded quote to be a literal substring of the folded
|
|
10
|
+
transcript. It is deliberately a literal-substring check, not a fuzzy
|
|
11
|
+
match -- a paraphrase must fail, and so must a quote that only differs in
|
|
12
|
+
case, because case is not whitespace.
|
|
13
|
+
|
|
14
|
+
On a real run of the sibling skill (doctor-md-agents), four judges all
|
|
15
|
+
claimed to have grep-verified their quotes and 2 of 62 findings still cited
|
|
16
|
+
text that was not there. This script exists so that claim is never taken on
|
|
17
|
+
faith again: SKILL.md aggregates only verified results, and the unverified
|
|
18
|
+
count is surfaced in the report rather than silently dropped.
|
|
19
|
+
|
|
20
|
+
Failure has four distinct reasons, and they are reported separately because
|
|
21
|
+
they mean different things to whoever reads results.verified.json:
|
|
22
|
+
|
|
23
|
+
quote_missing the result carries no usable quote text at all.
|
|
24
|
+
quote_too_short the quote is real but too short to prove anything
|
|
25
|
+
(a 3-character quote is trivially a substring).
|
|
26
|
+
not_found the transcript was read fine; the quote just is not
|
|
27
|
+
in it -- this is the scorer confabulating.
|
|
28
|
+
transcript_unreadable the transcript_path is missing or could not be
|
|
29
|
+
read -- an input-data problem, not a confabulation,
|
|
30
|
+
so it must never be folded into the scorer's error
|
|
31
|
+
rate.
|
|
32
|
+
|
|
33
|
+
Emits:
|
|
34
|
+
<out>/results.verified.json - every input result, in order, with every
|
|
35
|
+
original field preserved, plus `verified`
|
|
36
|
+
and `reason_code` (null when verified),
|
|
37
|
+
and a top-level `stats` block.
|
|
38
|
+
|
|
39
|
+
Python 3.9+, stdlib only. Reads only results_json and the transcript_path
|
|
40
|
+
files it names; writes only under --out; makes no network call; never
|
|
41
|
+
modifies a transcript or the input results file.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import json
|
|
46
|
+
import re
|
|
47
|
+
import sys
|
|
48
|
+
from collections import Counter
|
|
49
|
+
from datetime import datetime, timezone
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
WHITESPACE_RE = re.compile(r"\s+")
|
|
53
|
+
|
|
54
|
+
REASON_CODES = ("quote_missing", "quote_too_short", "not_found", "transcript_unreadable")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fold_whitespace(text):
|
|
58
|
+
"""Collapse every run of whitespace to a single space and strip ends.
|
|
59
|
+
|
|
60
|
+
Deliberately does NOT lowercase and does NOT strip punctuation -- this
|
|
61
|
+
must stay a literal-substring check, not a fuzzy match. A quote that
|
|
62
|
+
differs from the transcript only in case is a real verification
|
|
63
|
+
failure, not noise to smooth over.
|
|
64
|
+
"""
|
|
65
|
+
return WHITESPACE_RE.sub(" ", text).strip()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_transcript_text(path):
|
|
69
|
+
"""Read one transcript file. Returns None if it cannot be read.
|
|
70
|
+
|
|
71
|
+
A separate, patchable function so callers can count/observe real reads
|
|
72
|
+
even though results are cached per run -- see get_folded_transcript.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
return Path(path).expanduser().read_text(encoding="utf-8", errors="replace")
|
|
76
|
+
except OSError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_folded_transcript(path, cache):
|
|
81
|
+
"""Return the whitespace-folded transcript at `path`, cached per run.
|
|
82
|
+
|
|
83
|
+
Many results usually share a transcript_path (efficiency and
|
|
84
|
+
code-quality both score the same transcript); this makes sure each
|
|
85
|
+
distinct path is only read from disk once per run. `cache` is owned by
|
|
86
|
+
the caller (main), never a module-level dict, so nothing leaks between
|
|
87
|
+
separate script runs or between tests.
|
|
88
|
+
"""
|
|
89
|
+
if not isinstance(path, str) or not path:
|
|
90
|
+
return None
|
|
91
|
+
if path in cache:
|
|
92
|
+
return cache[path]
|
|
93
|
+
text = load_transcript_text(path)
|
|
94
|
+
folded = fold_whitespace(text) if text is not None else None
|
|
95
|
+
cache[path] = folded
|
|
96
|
+
return folded
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def verify_one(result, min_quote_chars, transcript_cache):
|
|
100
|
+
"""Return (verified, reason_code) for one scoring-result dict."""
|
|
101
|
+
quote_raw = result.get("quote")
|
|
102
|
+
if not isinstance(quote_raw, str) or not quote_raw.strip():
|
|
103
|
+
return False, "quote_missing"
|
|
104
|
+
|
|
105
|
+
folded_quote = fold_whitespace(quote_raw)
|
|
106
|
+
if len(folded_quote) < min_quote_chars:
|
|
107
|
+
return False, "quote_too_short"
|
|
108
|
+
|
|
109
|
+
folded_transcript = get_folded_transcript(result.get("transcript_path"), transcript_cache)
|
|
110
|
+
if folded_transcript is None:
|
|
111
|
+
return False, "transcript_unreadable"
|
|
112
|
+
|
|
113
|
+
if folded_quote in folded_transcript:
|
|
114
|
+
return True, None
|
|
115
|
+
return False, "not_found"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def transcript_key(result):
|
|
119
|
+
path = result.get("transcript_path")
|
|
120
|
+
return path if isinstance(path, str) and path else "(missing)"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def parse_args(argv=None):
|
|
124
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
125
|
+
p.add_argument("results_json", help="path to a results.json ({'results': [...]})")
|
|
126
|
+
p.add_argument("--out", required=True)
|
|
127
|
+
p.add_argument("--min-quote-chars", type=int, default=12,
|
|
128
|
+
help="minimum folded-quote length required to count as evidence (default: 12)")
|
|
129
|
+
return p.parse_args(argv)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_results_file(path):
|
|
133
|
+
"""Load and structurally validate the input file. Raises ValueError on
|
|
134
|
+
any malformed shape -- caller turns that into a non-zero exit."""
|
|
135
|
+
try:
|
|
136
|
+
raw = path.read_text(encoding="utf-8")
|
|
137
|
+
except OSError as exc:
|
|
138
|
+
raise ValueError(f"could not read {path}: {exc}") from exc
|
|
139
|
+
try:
|
|
140
|
+
data = json.loads(raw)
|
|
141
|
+
except json.JSONDecodeError as exc:
|
|
142
|
+
raise ValueError(f"{path} is not valid JSON: {exc}") from exc
|
|
143
|
+
if not isinstance(data, dict) or not isinstance(data.get("results"), list):
|
|
144
|
+
raise ValueError(f"{path} is not a valid results file (expected {{'results': [...]}})")
|
|
145
|
+
return data["results"]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main(argv=None):
|
|
149
|
+
args = parse_args(argv)
|
|
150
|
+
results_path = Path(args.results_json).expanduser()
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
results = load_results_file(results_path)
|
|
154
|
+
except ValueError as exc:
|
|
155
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
158
|
+
out_dir = Path(args.out).expanduser()
|
|
159
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
161
|
+
transcript_cache = {}
|
|
162
|
+
verified_results = []
|
|
163
|
+
reason_counts = Counter()
|
|
164
|
+
by_transcript = {}
|
|
165
|
+
verified_count = 0
|
|
166
|
+
skipped_non_object = 0
|
|
167
|
+
|
|
168
|
+
for result in results:
|
|
169
|
+
if not isinstance(result, dict):
|
|
170
|
+
skipped_non_object += 1
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
verified, reason_code = verify_one(result, args.min_quote_chars, transcript_cache)
|
|
174
|
+
if verified:
|
|
175
|
+
verified_count += 1
|
|
176
|
+
else:
|
|
177
|
+
reason_counts[reason_code] += 1
|
|
178
|
+
|
|
179
|
+
out_result = dict(result)
|
|
180
|
+
out_result["verified"] = verified
|
|
181
|
+
out_result["reason_code"] = reason_code
|
|
182
|
+
verified_results.append(out_result)
|
|
183
|
+
|
|
184
|
+
key = transcript_key(result)
|
|
185
|
+
bucket = by_transcript.setdefault(key, {"total": 0, "verified": 0, "unverified": 0})
|
|
186
|
+
bucket["total"] += 1
|
|
187
|
+
bucket["verified" if verified else "unverified"] += 1
|
|
188
|
+
|
|
189
|
+
total = len(verified_results)
|
|
190
|
+
stats = {
|
|
191
|
+
"total": total,
|
|
192
|
+
"verified": verified_count,
|
|
193
|
+
"unverified": total - verified_count,
|
|
194
|
+
"skipped_non_object_entries": skipped_non_object,
|
|
195
|
+
"by_reason_code": {code: reason_counts.get(code, 0) for code in REASON_CODES},
|
|
196
|
+
"by_transcript": by_transcript,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
report = {
|
|
200
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
201
|
+
"min_quote_chars": args.min_quote_chars,
|
|
202
|
+
"results": verified_results,
|
|
203
|
+
"stats": stats,
|
|
204
|
+
}
|
|
205
|
+
out_path = out_dir / "results.verified.json"
|
|
206
|
+
out_path.write_text(json.dumps(report, indent=2))
|
|
207
|
+
|
|
208
|
+
print(f"results.verified.json: {out_path}")
|
|
209
|
+
print(f"total results: {stats['total']}")
|
|
210
|
+
print(f"verified: {stats['verified']}")
|
|
211
|
+
print(f"unverified: {stats['unverified']}")
|
|
212
|
+
for code in REASON_CODES:
|
|
213
|
+
count = stats["by_reason_code"][code]
|
|
214
|
+
if count:
|
|
215
|
+
print(f" {code}: {count}")
|
|
216
|
+
if skipped_non_object:
|
|
217
|
+
print(f"warning: skipped {skipped_non_object} non-object result entries", file=sys.stderr)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|