| |
| """Pull lawyer feedback bundles from the private HF dataset and digest them. |
| |
| Feedback Mode (the Vercel UI toggle) posts judgment bundles to /api/feedback_bundle, |
| which the Space pushes durably to the private dataset vg15o2/themis-feedback |
| (one JSON per share under feedback/YYYY-MM-DD/). This script is the owner side: |
| download everything, print a digest, dump a flat JSONL, and optionally emit |
| qrels-candidate rows for the eval set. |
| |
| Grade map (placement -> graded relevance): top5=3, top10=2, after10=1, irrelevant=0. |
| |
| Usage: |
| python phase1/eval/fetch_feedback.py # digest + feedback_dump.jsonl |
| python phase1/eval/fetch_feedback.py --to-qrels # + feedback_qrels_candidates.tsv |
| Needs the HF write token (read suffices) in ~/.git-credentials or HF_TOKEN env. |
| """ |
| import glob, json, os, socket, subprocess, sys |
|
|
| _o = socket.getaddrinfo |
| socket.getaddrinfo = lambda h, p, f=0, *a, **k: _o(h, p, socket.AF_INET, *a, **k) |
|
|
| HERE = os.path.dirname(os.path.abspath(__file__)) |
| GRADE = {"top5": 3, "top10": 2, "after10": 1} |
|
|
| def token(): |
| t = os.environ.get("HF_TOKEN") |
| if t: return t |
| return subprocess.run(["bash", "-c", |
| "grep -m1 'huggingface.co' ~/.git-credentials | sed -E 's#https://[^:]*:([^@]+)@.*#\\1#'"], |
| capture_output=True, text=True).stdout.strip() |
|
|
| def main(): |
| from huggingface_hub import snapshot_download |
| local = snapshot_download("vg15o2/themis-feedback", repo_type="dataset", token=token()) |
| files = sorted(glob.glob(os.path.join(local, "feedback", "**", "*.json"), recursive=True)) |
| bundles = [] |
| for f in files: |
| data = json.load(open(f, encoding="utf-8")) |
| bundles.extend(data if isinstance(data, list) else [data]) |
| print(f"[feedback] {len(bundles)} bundle(s) across {len(files)} file(s)\n") |
|
|
| dump = os.path.join(HERE, "feedback_dump.jsonl") |
| with open(dump, "w", encoding="utf-8") as f: |
| for b in bundles: f.write(json.dumps(b, ensure_ascii=False) + "\n") |
|
|
| for b in bundles: |
| judged = [r for r in b.get("results", []) if r.get("placement") or r.get("irrelevant") or (r.get("comment") or "").strip()] |
| print(f"— {b.get('server_ts', b.get('ts',''))[:16]} {b.get('name','?')} [{b.get('mode','auto')}]") |
| print(f" Q: {b.get('q','')[:100]}") |
| for r in judged: |
| mark = "IRRELEVANT" if r.get("irrelevant") else (r.get("placement") or "") |
| c = (r.get("comment") or "").strip() |
| print(f" #{r.get('rank_shown','?'):>3} {mark:<10} {r.get('case_name','')[:52]}" + (f' "{c[:70]}"' if c else "")) |
| if (b.get("missing_case") or "").strip(): print(f" MISSING: {b['missing_case'][:100]}") |
| if (b.get("additional") or "").strip(): print(f" NOTE: {b['additional'][:140]}") |
| print() |
| print(f"[feedback] dump -> {dump}") |
|
|
| if "--to-qrels" in sys.argv: |
| out = os.path.join(HERE, "feedback_qrels_candidates.tsv") |
| with open(out, "w", encoding="utf-8") as f: |
| f.write("# qid(query text)\tdoc_id\tgrade\tsource\n") |
| for b in bundles: |
| for r in b.get("results", []): |
| if r.get("irrelevant"): g = 0 |
| elif r.get("placement") in GRADE: g = GRADE[r["placement"]] |
| else: continue |
| f.write(f"{b.get('q','')}\t{r['doc_id']}\t{g}\t{b.get('name','?')}\n") |
| print(f"[feedback] qrels candidates -> {out} (review before merging into the eval set)") |
|
|
| if __name__ == "__main__": |
| main() |
|
|