Download code_release/code/construction/pipeline/event_chain.py from clinical-agent-memory-bench/meme-bench: direct link, hf CLI and curl.
- Browser
- Download file 8.03 kB
-
https://huggingface.co/spaces/clinical-agent-memory-bench/meme-bench/resolve/main/code_release/code/construction/pipeline/event_chain.py
- Command line
-
hf download hf://spaces/clinical-agent-memory-bench/meme-bench/code_release/code/construction/pipeline/event_chain.py
-
curl -L -o event_chain.py https://huggingface.co/spaces/clinical-agent-memory-bench/meme-bench/resolve/main/code_release/code/construction/pipeline/event_chain.py
8.03 kB
| #!/usr/bin/env python3 | |
| """Stage B, deterministic parts of the event chain and the visit-level change annotation. | |
| event-chain RECORDS extraction records -> event_chain.jsonl | |
| windows change-annotation windows for the tags step -> _tag_manifest.json | |
| tags-post TAGS change tags of the tags step -> tags.jsonl | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import glob | |
| import json | |
| import os | |
| import re | |
| import sys | |
| from collections import Counter | |
| from pipeline.common import BASE, FIELDS, patient_id | |
| PATIENT = patient_id() | |
| def event_chain(records_path: str) -> None: | |
| """Merge the per-visit extractions with visit metadata, ICD notes and vitals, check grounding and | |
| cross-bucket duplication, and write event_chain.jsonl.""" | |
| visits = {json.loads(l)["visit_number"]: json.loads(l) for l in open(f"{BASE}/visits.jsonl")} | |
| VISITS = sorted(visits) | |
| if os.path.exists(f"{BASE}/vitals.json"): | |
| orig_vitals = {int(k): v for k, v in json.load(open(f"{BASE}/vitals.json")).items()} | |
| else: | |
| sys.exit(f"FATAL: no vitals source in {BASE}. Run the vitals step first.") | |
| raw = json.load(open(records_path)) | |
| records = raw["records"] if isinstance(raw, dict) else raw | |
| by_v = {r["visit_number"]: r for r in records} | |
| WORD = re.compile(r"[a-z0-9]+") | |
| def toks(s): | |
| return [w for w in WORD.findall(str(s).lower()) if len(w) >= 3] | |
| def gfrac(a, src): | |
| t = toks(a) | |
| return 1.0 if not t else sum(1 for w in t if w in src) / len(t) | |
| chain, gs, weak, dups = [], [], [], [] | |
| for v in VISITS: | |
| meta = visits[v] | |
| rec = by_v.get(v, {}) | |
| src = set(toks(" ".join(meta["soap"].values()))) | |
| out = {"patient_id": PATIENT, "visit_number": v, "date": meta["date"], "department": meta["department"], | |
| "icd_notes": meta.get("icd") or []} | |
| for f in FIELDS: | |
| out[f] = rec.get(f, []) or [] | |
| for a in out[f]: | |
| g = gfrac(a, src) | |
| gs.append(g) | |
| if g < 0.5: | |
| weak.append((v, f, a[:50], round(g, 2))) | |
| out["vitals"] = orig_vitals.get(v, {}) | |
| seen = {} | |
| for f in FIELDS: | |
| for a in out[f]: | |
| k = re.sub(r"\s+", " ", str(a).lower()).strip() | |
| if k in seen and seen[k] != f: | |
| dups.append((v, k[:40], seen[k], f)) | |
| seen[k] = f | |
| chain.append(out) | |
| with open(f"{BASE}/event_chain.jsonl", "w", encoding="utf-8") as fh: | |
| for c in chain: | |
| fh.write(json.dumps(c, ensure_ascii=False) + "\n") | |
| n = len(gs) | |
| miss = [v for v in VISITS if v not in by_v] | |
| print(f"patient: {PATIENT} visits: {min(VISITS)}..{max(VISITS)}") | |
| print(f"records: {len(chain)} events: {n} mean_grounding: {sum(gs)/n if n else 0:.3f}") | |
| print(f"vitals: {sum(1 for c in chain if c['vitals'])} visits carry >=1") | |
| print(f"missing visits: {len(miss)} {miss[:20]}") | |
| print(f"weak events (<0.5): {len(weak)} ({100*len(weak)/max(1,n):.1f}%)") | |
| print(f"cross-bucket duplicates: {len(dups)}") | |
| for w in weak[:12]: | |
| print(" WEAK", w) | |
| for d in dups[:12]: | |
| print(" DUP", d) | |
| for f in FIELDS: | |
| print(f" {f:26} visits_with={sum(1 for c in chain if c[f]):3} events={sum(len(c[f]) for c in chain)}") | |
| print("wrote event_chain.jsonl") | |
| def windows() -> None: | |
| """Tags-step preparation: per visit, up to two most recent prior visits in the same department and the | |
| most recent visit in a different department. Writes batch files and a manifest.""" | |
| chain = [json.loads(l) for l in open(f"{BASE}/event_chain.jsonl")] | |
| chain.sort(key=lambda r: r["visit_number"]) | |
| by_v = {r["visit_number"]: r for r in chain} | |
| def facts(r): | |
| return {f: r.get(f, []) for f in FIELDS} | |
| prev_by_dept = {} | |
| items = [] | |
| for r in chain: | |
| v = r["visit_number"] | |
| dept = r["department"] | |
| prevs = prev_by_dept.get(dept, []) | |
| tprev = prevs[-2:] | |
| xdept = None | |
| for pr in reversed(chain[:chain.index(r)]): | |
| if pr["department"] != dept: | |
| xdept = pr["visit_number"] | |
| break | |
| items.append({ | |
| "visit_number": v, "date": r["date"], "department": dept, | |
| "is_first_in_dept": len(prevs) == 0, | |
| "prev_visits": [p for p in tprev], | |
| "prev_cross_dept_visit": xdept, | |
| "current_facts": facts(r), | |
| "prev_facts": [{"visit_number": p, "date": by_v[p]["date"], "facts": facts(by_v[p])} for p in tprev], | |
| }) | |
| prev_by_dept.setdefault(dept, []).append(v) | |
| bdir = f"{BASE}/_tag_batches" | |
| os.makedirs(bdir, exist_ok=True) | |
| for f in os.listdir(bdir): | |
| os.remove(os.path.join(bdir, f)) | |
| SZ = 20 | |
| manifest = [] | |
| for i in range(0, len(items), SZ): | |
| ch = items[i:i+SZ] | |
| p = os.path.join(bdir, f"tag_{i//SZ:02d}.json") | |
| json.dump(ch, open(p, "w"), ensure_ascii=False) | |
| manifest.append({"batch_id": i//SZ, "path": p, "n": len(ch), | |
| "first_v": ch[0]["visit_number"], "last_v": ch[-1]["visit_number"]}) | |
| json.dump(manifest, open(f"{BASE}/_tag_manifest.json", "w")) | |
| n_first = sum(1 for it in items if it["is_first_in_dept"]) | |
| print(f"visits: {len(items)} | first-in-dept: {n_first} | batches: {len(manifest)} (size {SZ})") | |
| print("example item:", json.dumps(items[0], ensure_ascii=False)[:500]) | |
| print("manifest:", json.dumps(manifest)) | |
| def tags_post(tags_path: str) -> None: | |
| """Merge the change tags with the window metadata into tags.jsonl, with statistics.""" | |
| items = {} | |
| for p in glob.glob(f"{BASE}/_tag_batches/*.json"): | |
| for it in json.load(open(p)): | |
| items[it["visit_number"]] = it | |
| raw = json.load(open(tags_path)) | |
| tagrows = raw["tags"] if isinstance(raw, dict) else raw | |
| tagmap = {t["visit_number"]: t for t in tagrows} | |
| out = [] | |
| for v in sorted(items): | |
| it = items[v] | |
| t = tagmap.get(v, {"tag": "MISSING", "noise_score": -1, "note": ""}) | |
| out.append({ | |
| "visit_number": v, "date": it["date"], "department": it["department"], | |
| "prev_visits": it["prev_visits"], "prev_cross_dept_visit": it["prev_cross_dept_visit"], | |
| "tag": t["tag"], "noise_score": t["noise_score"], "note": t["note"], | |
| }) | |
| with open(f"{BASE}/tags.jsonl", "w", encoding="utf-8") as fh: | |
| for o in out: | |
| fh.write(json.dumps(o, ensure_ascii=False) + "\n") | |
| tagc = Counter(o["tag"] for o in out) | |
| scorec = Counter(o["noise_score"] for o in out) | |
| print(f"tags: {len(out)}/{len(items)} | tag dist: {dict(tagc)} " | |
| f"| noise_score dist: {dict(sorted(scorec.items()))}") | |
| first = [o["visit_number"] for o in out if items[o["visit_number"]]["is_first_in_dept"] | |
| and not (o["tag"] == "change" and o["noise_score"] == 0)] | |
| print(f"first-in-dept visits not tagged change/0: {len(first)} {first[:15]}") | |
| by_dept = {} | |
| for o in out: | |
| d = o["department"].split(" (")[0] | |
| by_dept.setdefault(d, []).append(o) | |
| print("per-dept no_change share:") | |
| for d, rows in sorted(by_dept.items(), key=lambda x: -len(x[1])): | |
| nc = sum(1 for r in rows if r["tag"] == "no_change") | |
| mean = sum(r["noise_score"] for r in rows) / len(rows) | |
| print(f" {d:24s} {len(rows):4d} visits no_change={nc:4d} ({100*nc/len(rows):.0f}%) meanScore={mean:.2f}") | |
| print("wrote tags.jsonl") | |
| def main(): | |
| ap = argparse.ArgumentParser(description="Stage B: event chain and change annotation.") | |
| sp = ap.add_subparsers(dest="cmd", required=True) | |
| sp.add_parser("event-chain").add_argument("records") | |
| sp.add_parser("windows") | |
| sp.add_parser("tags-post").add_argument("tags") | |
| a = ap.parse_args() | |
| if a.cmd == "event-chain": | |
| event_chain(a.records) | |
| elif a.cmd == "windows": | |
| windows() | |
| else: | |
| tags_post(a.tags) | |
| if __name__ == "__main__": | |
| main() | |