"""Local lead-reviewer synthesis — no frontier model. After the panel finishes, one more hardened container (the "lead reviewer") reads all the individual reports (mounted read-only at /reports) plus the documents (/docs), runs a configured local model, and writes a single consolidated report to /out/CONSOLIDATED_REPORT.md: shared themes, where reviewers disagree, the consensus, and an overall recommendation. It reuses the same one-shot reviewer image, switched to BM_ROLE=synthesizer. """ from __future__ import annotations import shlex import spark_client as sc import serving DEFAULT_LEAD_PERSONA = ( "You are the lead reviewer chairing the panel. You did not read the documents " "first-hand for a fresh opinion — your job is to CONSOLIDATE the panel's " "individual reports into one authoritative report. Identify the findings the " "reviewers agree on, surface and adjudicate where they conflict, note anything " "only one reviewer caught, and end with a prioritized recommendation. Attribute " "points to the reviewer(s) who raised them. Do not invent findings." ) def pick_model(cfg: dict) -> str: alias = (cfg.get("synthesisModel") or "").strip() if alias: return alias models = cfg.get("models") or [] return models[0]["alias"] if models else "" def run_synthesis(cfg: dict, jobdir: str, rubric: str, log, wait_timeout: int = 1800) -> dict: """Launch the lead-reviewer container and wait for the consolidated report.""" head = sc.head(cfg) q = shlex.quote model = pick_model(cfg) if not model: raise RuntimeError("no model available for synthesis (configure a model catalog)") persona = (cfg.get("synthesisPersona") or "").strip() or DEFAULT_LEAD_PERSONA net = serving.net_name(cfg) base = serving.reviewer_proxy_base(cfg) rid = "lead-reviewer" sc.run(head, f"mkdir -p {q(jobdir)}/personas {q(jobdir)}/synth-out && " f"printf '%s' {q(persona)} > {q(jobdir)}/personas/{rid}.md && " f"printf '%s' {q(rubric)} > {q(jobdir)}/RUBRIC.md", timeout=30) env = ( f"-e BM_REVIEWER_ID={q(rid)} -e BM_REVIEWER_NAME=lead-reviewer -e BM_ROLE=synthesizer " f"-e BM_MODEL={q(model)} -e BM_LLM_BASE={q(base)} -e BM_LLM_KEY=sk-local " f"-e BM_MAX_MODEL_LEN={q(str(int(cfg.get('maxModelLen', 32768))))} -e HOME=/home/rev " ) harden = ( "--user 1000:1000 --security-opt no-new-privileges --cap-drop ALL " "--read-only --tmpfs /tmp:size=256m --tmpfs /home/rev:size=128m " "--pids-limit 256 --memory 6g --cpus 4" ) mounts = ( f"-v {q(jobdir)}/docs:/docs:ro " f"-v {q(jobdir)}/out:/reports:ro " f"-v {q(jobdir)}/synth-out:/out " f"-v {q(jobdir)}/personas/{rid}.md:/persona/PERSONA.md:ro " f"-v {q(jobdir)}/RUBRIC.md:/RUBRIC.md:ro " ) cname = f"bm-grader-{rid}" cmd = ( f"docker rm -f {cname} >/dev/null 2>&1; " f"docker run -d --name {cname} --network {q(net)} {harden} {env} {mounts} {q(cfg['graderImage'])}" ) log(f"[synthesis] lead reviewer up -> {model}") r = sc.run(head, cmd, timeout=120) if r.returncode != 0: raise RuntimeError(f"synthesis launch failed: {r.stderr or r.stdout}") w = sc.run(head, f"docker wait {cname}", timeout=wait_timeout) code = (w.stdout or "").strip() chk = sc.run(head, f"test -s {q(jobdir)}/synth-out/CONSOLIDATED_REPORT.md && echo OK || echo MISSING", timeout=30) wrote = "OK" in (chk.stdout or "") sc.run(head, f"docker rm -f {cname} 2>/dev/null; true", timeout=30) log(f"[synthesis] lead reviewer exited (code={code or '?'}), report={'written' if wrote else 'MISSING'}") return {"model": model, "exit": code, "report": wrote}