From 471934cc2c82dd1f631a624772b75712587be7cb Mon Sep 17 00:00:00 2001 From: Chaim Date: Wed, 17 Jun 2026 10:42:14 +0000 Subject: [PATCH] =?UTF-8?q?feat(operations):=20=D7=94=D7=95=D7=A1=D7=A4?= =?UTF-8?q?=D7=AA=20codex=5Flocal=20=D7=9C=D7=A1=D7=A8=D7=92=D7=9C-=D7=97?= =?UTF-8?q?=D7=99=D7=A8=D7=95=D7=9D=20+=20=D7=A1=D7=A7=D7=A8=D7=99=D7=A4?= =?UTF-8?q?=D7=98=20A/B-benchmark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adapter_profiles.py: codex_local.default_model = gpt-5.5 (עדכון מ-gpt-5.3-codex) - agent-adapters-panel.tsx: כפתור "העבר הכל ל-Codex ⚡" בסרגל-החירום (ב-G/Codex fallback) - operations.ts: הוספת codex_local לדוקומנטציה של useAdapterMigrate - ab_halacha_codex.py: סקריפט A/B חדש — חילוץ הלכות דרך codex exec/gpt-5.5 (non-destructive benchmark) - SCRIPTS.md: תיעוד ab_halacha_codex.py Co-Authored-By: Claude Sonnet 4.6 --- scripts/SCRIPTS.md | 7 +- scripts/ab_halacha_codex.py | 278 ++++++++++++++++++ scripts/adapter_profiles.py | 28 +- .../operations/agent-adapters-panel.tsx | 14 +- web-ui/src/lib/api/operations.ts | 2 +- 5 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 scripts/ab_halacha_codex.py diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 3b3d767..a0012d6 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -14,7 +14,8 @@ |--------|------|---------|-----------| | `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh [BODY_JSON]`. אסור `curl` ישיר ל-`$PAPERCLIP_API_URL`. ראה `HEARTBEAT.md §0`. counterpart ב-Python: `web/paperclip_api.py`. | נקרא ע"י סוכנים | | `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** **⚠ אם `adapter_type` שונה בין CMP ל-CMPA — `--apply` מדלג על הסוכן; `--verify` מדווח אותו רם כ-DRIFT.** בעת מעבר adapter (למשל ל-`deepseek_local`) חובה לעדכן ידנית בשתי החברות. **`--verify` יוצא exit≠0 על כל drift** (needs-sync / adapter-mismatch / missing-in-mirror) — שמיש כ-gate ל-cron/CI (GAP-21/FU-8a). | ידני אחרי כל שינוי | -| `adapter_profiles.py` | python (module) | **רישום-פרופילי-אדפטר** — מקור-אמת יחיד ל-3 צירי-הכשל של מעבר-אדפטר: provider/default_model, instructions_mode (`file_path` בטוח-frontmatter מול `content_arg` ששובר `---`), ו-tool_config (`gemini_global` excludeTools / `frontmatter` / `hermes`). מיובא ע"י `migrate_agent_adapter.py`. הוספת אדפטר עתידי = רשומה אחת. לא מורץ ישירות. | תשתית | +| `fix_paperclipai_skills_drift.py` | python | סקריפט חד-פעמי (בוצע 2026-05-04) שניקה drift על `paperclipai/*` skills בין CMP ל-CMPA. הסיר `paperclip-dev` מכל 14 הסוכנים, ודאג ש-`paperclip-converting-plans-to-tasks` קיים רק על CEO ו-analyst. תומך `--apply` (ברירת מחדל: dry-run). דורש `PAPERCLIP_BOARD_API_KEY`. נשמר לרפרנס למקרה שhdrift חוזר. | חד-פעמי (בוצע) | +| `adapter_profiles.py` | python (module) | **רישום-פרופילי-אדפטר** — מקור-אמת יחיד ל-3 צירי-הכשל של מעבר-אדפטר: provider/default_model, instructions_mode (`file_path` בטוח-frontmatter מול `content_arg` ששובר `---`), ו-tool_config (`gemini_global` excludeTools / `frontmatter` / `hermes` / `codex_home`). כולל `codex_local` עם משפחת מודלי OpenAI/Codex (`gpt-*`, `o3*`, `o4*`, `codex-*`). מיובא ע"י `migrate_agent_adapter.py`. הוספת אדפטר עתידי = רשומה אחת. לא מורץ ישירות. | תשתית | | `migrate_agent_adapter.py` | python | **מעבר-אדפטר בטוח לכל סוכן ← כל אדפטר, בשתי החברות יחד (INV-MC1)**. מיישב model↔provider, גורס frontmatter לעותק `.generated/.nofm.md` ל-content_arg adapters (אחרת קריסת `gemini --prompt`/`hermes -q` על `---`), ומשחרר excludeTools גלובלי של gemini (`--relax-tools`). `--check` (preflight בלבד, exit≠0 על שגיאה — שער FU-8a) / `--apply` / `--revert` (שחזור מדויק מ-sidecar `data/adapter-migration-state.json`) / `--verify` (מסמן מצב לא-תואם/א-סימטרי, exit≠0). `--agent "<שם>"\|all --to [--model X] [--relax-tools]`. PATCH דרך `/api/agents/{id}` (לא DB). דורש `PAPERCLIP_BOARD_API_KEY`. הרץ עם `mcp-server/.venv/bin/python`. **fallback-חירום כשנגמרים טוקני-Claude; החזר ל-claude_local כשחוזרים.** | ידני לפי צורך | ### אחזור, embeddings ו-multimodal @@ -64,7 +65,9 @@ | `halacha_panel_calibrate.py` | python | **כיול + מדידת הפאנל** (Trust-or-Escalate, ICLR 2025). `--source live` (ברירת-מחדל): מריץ את שאלת-ה-KEEP על מדגם-הזהב ומודד מול `is_holding` precision+coverage+**split-rate** לכל מדיניות + false-keep/false-drop (מייבא שופטים מ-`halacha_panel_approve`, **חובה מקומי**). **#133/FU-5** — `--source captured`: **אפס-עלות** (בלי re-vote/LLM) — מצליב סבבים שמורים (FU-1) מול הכרעות-יו"ר (FU-2) דרך `db.panel_rounds_vs_chair` ומדווח split-rate+auto-precision **לכל סבב** (מגמת הלולאה: ככל שהרובריקה משתפרת precision נשמר ו-split יורד); משתף את `analyze_pairs` של FU-4 (מקור-יחיד). שתי המדידות מדווחות **anon-stability** (מבחן-אנונימיזציה #81.7) כמטריקת-בריאות נגד echo-chamber. `--batch`/`--limit`/`--concurrency`. | ידני — לפני חיווט `--apply` (live) / תקופתי — מעקב-לולאה (captured) | | `halacha_rubric_distill.py` | python | **#133/FU-4 — זיקוק-רובריקה PROPOSE-ONLY.** מצליב `halacha_panel_rounds` (FU-1, הצבעות+נימוקים) מול הכרעות-היו"ר (FU-2, seeds ב-`halacha_goldset` batch `chair-live`) דרך `db.panel_rounds_vs_chair` (read-only), מנתח דטרמיניסטית **כשלים שיטתיים** (false-keep/false-drop, פיצולים-שהוכרעו, שיעור-מחלוקת-עם-היו"ר לכל שופט), ומציע `KEEP_SYSTEM` v2 + exemplars מופשטים (claude_session מקומי, אפס עלות) כ**דוח-diff** ל-`data/learning/rubric-proposal-.md`. **לעולם לא auto-apply** — אימוץ v2 = עריכה אנושית של הקבוע דרך PR (INV-LRN1); exemplars מופשטים בלבד (INV-LRN5); הסיגנל היחיד = הכרעת-יו"ר, לא הצבעות-פאנל (anti-echo). מתחת ל-12 זוגות → "אין מספיק נתונים". `--no-llm` (סטטיסטיקה בלבד) / `--limit N`. **חובה מקומי**. | תקופתי — אחרי שהצטברו הכרעות-יו"ר על מחלוקות-פאנל | | `halacha_batch_reconcile.py` | python | **#82.7** — dedup חוצה-פסקים offline (שמרני, **dry-run בלבד**). dedup-on-insert משווה רק תוך-פסק; כאן סף מחמיר (cosine ≥0.95, `--cosine`) ולא-הרסני: מאתר זוגות הלכות near-duplicate בין פסקים שונים (pgvector `<=>` exact) עם איתות לקסיקלי (Jaccard/Levenshtein) ומדווח ל-CSV ב-`data/audit/` לסקירת היו"ר. לא מדלג/ממזג/מוחק. `--include-pending`. **`--link`** רושם את הזוגות שנמצאו כ-`equivalent_halachot` (parallel authority, #84.2 — קישור-מקביל ברמת-הלכה, **לא** ציטוט; idempotent, לא-הרסני). רץ עם venv של mcp-server. אומת: 800 הלכות → 5 זוגות (קושרו). | ידני — דוח-סקירה / `--link` לקישור | -| `ab_halacha_opus48.py` | python | **A/B לא-הרסני לחילוץ הלכות** — מריץ מחדש חילוץ הלכות על פסק-דין בודד דרך מודל/effort נבחרים (`AB_MODEL`/`AB_EFFORT`, ברירת-מחדל `claude-opus-4-8`/`xhigh`) ומשווה לסטטיסטיקות ההלכות הקיימות ב-DB **בלי למחוק/לכתוב כלום**. משכפל את `halacha_extractor.extract()` (אותם פרומפטים, בחירת-צ'אנקים, אימות-ציטוט) ומחליף רק את קריאת ה-LLM ב-`claude -p --model --effort`. מפיק `data/ab_halacha__.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data .venv/bin/python scripts/ab_halacha_opus48.py `. **ממצא 2026-05-31 (שטיין 1128-08-20):** Opus 4.8@xhigh חילץ 51 מול 124 בייצור (100% quote-verified מול 96%) אך ביטחון מכויל-נמוך יותר (חציון 0.75 מול 0.82) — ולכן **לא** מקטין את תור-האישור-הידני תחת sweep אוטו-אישור conf≥0.78 (26 מול 24). שיפור איכות, לא צמצום-תור. | ידני (החלטת מודל-חילוץ) | +| `calibrate_halacha_dedup.py` | python | **#82.1** — כיול ספי ה-dedup הלקסיקלי (#82.3) מול gold-set הניקוי. קורא `halacha-cleanup-manifest-*.csv` (זוגות duplicate↔survivor מתויגי-אדם), טוען טקסט-survivor מה-DB, ו-sweep של (jaccard_min × levenshtein_min) עם P/R/F1, מסמן את נקודת-העבודה המוגדרת. אימת ש-(0.55, 0.70) → **precision 1.0** (אפס false-merge), recall 0.30 — מתאים לאיתות-משני שחוסם auto-approve. `--manifest `. רץ עם venv של mcp-server | חד-פעמי — כיול (בוצע 2026-06-06) | +| `ab_halacha_opus48.py` | python | **A/B לא-הרסני לחילוץ הלכות (Claude)** — מריץ מחדש חילוץ הלכות על פסק-דין בודד דרך מודל/effort נבחרים (`AB_MODEL`/`AB_EFFORT`, ברירת-מחדל `claude-opus-4-8`/`xhigh`) ומשווה לסטטיסטיקות ההלכות הקיימות ב-DB **בלי למחוק/לכתוב כלום**. משכפל את `halacha_extractor.extract()` (אותם פרומפטים, בחירת-צ'אנקים, אימות-ציטוט) ומחליף רק את קריאת ה-LLM ב-`claude -p --model --effort`. מפיק `data/ab_halacha__.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data .venv/bin/python scripts/ab_halacha_opus48.py `. **ממצא 2026-05-31 (שטיין 1128-08-20):** Opus 4.8@xhigh חילץ 51 מול 124 בייצור (100% quote-verified מול 96%) אך ביטחון מכויל-נמוך יותר (חציון 0.75 מול 0.82) — ולכן **לא** מקטין את תור-האישור-הידני תחת sweep אוטו-אישור conf≥0.78 (26 מול 24). שיפור איכות, לא צמצום-תור. | ידני (החלטת מודל-חילוץ) | +| `ab_halacha_codex.py` | python | **A/B לא-הרסני לחילוץ הלכות (Codex/gpt-5.5)** — עמית ל-`ab_halacha_opus48` אך מחליף את `claude -p` ב-`codex exec --model gpt-5.5` (אימות ChatGPT, ללא OPENAI_API_KEY). אותם פרומפטים ואותו הסקת quote-verification. הפלט האחרון של הסוכן (`-o FILE`) נפענח כ-JSON. `AB_MODEL` (default `gpt-5.5`), `AB_REASONING` low/medium/high/xhigh (default `medium`), `AB_CONCURRENCY` (default 1), `CODEX_BIN`. מפיק `data/ab_halacha_codex___.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data mcp-server/.venv/bin/python scripts/ab_halacha_codex.py `. **ממצא ראשוני — 8181-21 האוניברסיטה העברית:** ממתין להרצה (מול 28 הלכות Opus). | ידני (בנצ'מרק מודל codex) | | `monitor_halacha_quality.py` | python | מנטר איכות חילוץ הלכות. בודק drift של `avg(confidence)` בין baseline היסטורי לחלון אחרון. מחזיר JSON מטריקות + alert ב-stderr אם drift > threshold (ברירת מחדל 5%). 2 סדרות: trusted (approved+published) ו-all_extracted. תומך `--window N` / `--threshold X` / `--min-sample N` / `--silent` / `--exit-on-alert`. רץ ב-container או מקומית עם `mcp-server/.venv` (אין תלות ב-LLM, רק SQL). **תזמון מומלץ**: `0 8 * * 1` (יום ראשון 08:00, שבועי) | `0 8 * * 1` (לתזמן) | | `audit_training_corpus.py` | python | audit של `style_corpus` — לכל החלטה: שדות מטא-דאטה מאוכלסים (`summary`/`outcome`/`key_principles`/`appeal_subtype`/`subject_categories`), קישור ל-`documents` (FK + chunks + embeddings). מפיק `data/audit/corpus-YYYY-MM-DD.json` + summary בקונסול. דרוש `POSTGRES_URL` או POSTGRES_*. אין תלויות חיצוניות מלבד asyncpg. **רץ מהמכונה המקומית** (לא קונטיינר) — חיבור ישיר ל-Postgres :5433 | ידני / קדם-עבודה לפני enrichment של מטא-דאטה | | `backfill_style_exemplars.py` | python | **T1 (style-acquisition)** — מאכלס `style_exemplars` מקורפוס דפנה (`style_corpus` + `internal_committee` chair=דפנה): מפצל לסעיפים (`chunker._split_into_sections`) → פסקאות (25-450 מילים) → embed (Voyage) → שמירה עם `section`/`outcome`/`practice_area`. מאפשר לכותב לאחזר פסקאות-בלוק אמיתיות של דפנה (T2/T3). מקור-סגנון בלבד (INV-LRN5). אידמפוטנטי (מנקה per-decision). `--dry-run` (default) / `--apply`. דורש POSTGRES_URL + Voyage. **רץ מקומית** (venv). | ידני (`python scripts/backfill_style_exemplars.py --apply`) | diff --git a/scripts/ab_halacha_codex.py b/scripts/ab_halacha_codex.py new file mode 100644 index 0000000..deb8091 --- /dev/null +++ b/scripts/ab_halacha_codex.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +"""A/B (NON-DESTRUCTIVE): re-extract halachot for ONE precedent via the +Codex CLI (gpt-5.5) and compare against the existing stored halachot. + +Purpose: benchmark gpt-5.5 against the current Claude Opus production output — +WITHOUT deleting or storing anything in the DB. + +Mirrors ab_halacha_opus48.py but replaces `claude -p` with + `codex exec --model gpt-5.5 --dangerously-bypass-approvals-and-sandbox -o FILE -` +The model's last message (written to FILE via `-o`) is parsed as JSON. + +Usage: + DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data \ + AB_MODEL=gpt-5.5 AB_REASONING=medium \ + mcp-server/.venv/bin/python scripts/ab_halacha_codex.py + +Env knobs: + AB_MODEL model slug (default gpt-5.5) + AB_REASONING reasoning effort: low/medium/high/xhigh (default medium) + AB_CONCURRENCY concurrent chunks (default 1 — codex sessions, be conservative) + AB_CHUNK_TIMEOUT seconds per chunk (default 300) + CODEX_BIN path to codex binary (default: VS Code extension arm64 build) +""" +from __future__ import annotations + +import asyncio +import json +import os +import statistics +import sys +import tempfile +from collections import Counter +from pathlib import Path +from uuid import UUID + +from legal_mcp.config import parse_llm_json +from legal_mcp.services import db +from legal_mcp.services import halacha_extractor as hx + +# ── configuration ───────────────────────────────────────────────────────────── + +MODEL = os.environ.get("AB_MODEL", "gpt-5.5") +REASONING = os.environ.get("AB_REASONING", "medium") +CONCURRENCY = int(os.environ.get("AB_CONCURRENCY", "1")) +CHUNK_TIMEOUT = int(os.environ.get("AB_CHUNK_TIMEOUT", "300")) + +# ARM64 build bundled with the VS Code ChatGPT extension — authenticated via +# ~/.codex/auth.json (ChatGPT subscription, no OPENAI_API_KEY needed). +CODEX_BIN = os.environ.get( + "CODEX_BIN", + "/home/chaim/.vscode-server/extensions/" + "openai.chatgpt-26.609.30741-linux-arm64/bin/linux-aarch64/codex", +) + + +# ── codex invocation ────────────────────────────────────────────────────────── + +async def run_codex(system: str, prompt: str, timeout: int = CHUNK_TIMEOUT): + """One `codex exec` call. Returns parsed JSON from the model's last message. + + Codex is an agentic runner; we steer it to output-only mode via an explicit + instruction prepended to the system prompt. The `-o FILE` flag captures the + final text message; `parse_llm_json` strips any markdown fences. + """ + preamble = ( + "Your output MUST be a valid JSON array and nothing else.\n" + "Do NOT use shell commands, do NOT write files.\n" + "Respond ONLY with the JSON array — no explanation, no markdown fences.\n\n" + ) + full_input = preamble + system + "\n\n" + prompt + + out_fd, out_path = tempfile.mkstemp(suffix=".txt", prefix="codex_ab_") + os.close(out_fd) + + cmd = [ + CODEX_BIN, "exec", + "--model", MODEL, + "-c", f"model_reasoning_effort={json.dumps(REASONING)}", + "--dangerously-bypass-approvals-and-sandbox", + "--skip-git-repo-check", + "--ephemeral", + "-o", out_path, + "-", + ] + try: + proc = await asyncio.create_subprocess_exec( + *cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.PIPE, + env={**os.environ, "HOME": "/home/chaim"}, + ) + _, err_b = await asyncio.wait_for( + proc.communicate(input=full_input.encode("utf-8")), + timeout=timeout, + ) + if proc.returncode != 0: + raise RuntimeError( + f"codex exit {proc.returncode}: " + f"{err_b.decode('utf-8', 'replace').strip()[:400]}" + ) + raw = Path(out_path).read_text(encoding="utf-8").strip() + finally: + Path(out_path).unlink(missing_ok=True) + + if not raw: + raise RuntimeError("codex returned empty last-message") + return parse_llm_json(raw) + + +# ── extraction (mirrors ab_halacha_opus48) ──────────────────────────────────── + +async def extract_chunk(chunk_text, section_type, idx, total, context, is_binding): + base_prompt = ( + hx.HALACHA_EXTRACTION_PROMPT_BINDING if is_binding + else hx.HALACHA_EXTRACTION_PROMPT_PERSUASIVE + ) + chunk_label = f" (חלק {idx + 1}/{total})" if total > 1 else "" + user_msg = ( + f"## הקלט\n" + f"סוג קטע: {section_type}\n" + f"{context}{chunk_label}\n\n" + f"--- תחילת הטקסט ---\n{chunk_text}\n--- סוף הטקסט ---" + ) + try: + result = await run_codex(base_prompt, user_msg) + except Exception as e: + print(f" ! chunk {idx + 1}/{total} failed: {e}", file=sys.stderr) + return [], False + if isinstance(result, list): + return result, True + print(f" ! chunk {idx + 1}/{total} non-list: {type(result).__name__}", file=sys.stderr) + return [], False + + +# ── statistics ──────────────────────────────────────────────────────────────── + +def stats(halachot: list[dict], label: str) -> dict: + n = len(halachot) + + def fconf(x): + try: + return float(x.get("confidence")) + except (TypeError, ValueError): + return None + + confs = [c for c in (fconf(h) for h in halachot) if c is not None] + qv = Counter(bool(h.get("quote_verified")) for h in halachot) + rt = Counter(h.get("rule_type") for h in halachot) + return { + "label": label, "n": n, + "quote_verified_true": qv.get(True, 0), + "quote_verified_false": qv.get(False, 0), + "conf_min": min(confs) if confs else None, + "conf_median": statistics.median(confs) if confs else None, + "conf_max": max(confs) if confs else None, + "conf_below_0_7": sum(1 for c in confs if c < 0.7), + "rule_types": dict(rt), + } + + +def print_stats(s: dict): + print(f"\n=== {s['label']} ===") + print(f" count : {s['n']}") + print(f" quote_verified : {s['quote_verified_true']} ✓ / {s['quote_verified_false']} ✗") + if s["conf_median"] is not None: + print(f" confidence min/med/max: {s['conf_min']:.2f} / {s['conf_median']:.2f} / {s['conf_max']:.2f}") + print(f" confidence < 0.7 : {s['conf_below_0_7']} / {s['n']}") + print(f" rule_type dist : {s['rule_types']}") + + +# ── main ────────────────────────────────────────────────────────────────────── + +async def main(): + if len(sys.argv) < 2: + print( + "usage: ab_halacha_codex.py \n" + " case_law_id: UUID of the case_law row (e.g. 246a22a0-46b5-...)\n" + "env: AB_MODEL (default gpt-5.5), AB_REASONING (default medium),\n" + " AB_CONCURRENCY (default 1), AB_CHUNK_TIMEOUT (default 300)", + file=sys.stderr, + ) + sys.exit(2) + + if not Path(CODEX_BIN).exists(): + print(f"codex binary not found: {CODEX_BIN}", file=sys.stderr) + print("set CODEX_BIN= to the correct path", file=sys.stderr) + sys.exit(1) + + case_law_id = UUID(sys.argv[1]) + + record = await db.get_case_law(case_law_id) + if not record: + print("case_law not found", file=sys.stderr) + sys.exit(1) + + is_binding = bool(record.get("is_binding")) + citation = record.get("case_number", "") + court = record.get("court", "") + date_str = str(record.get("date") or "") + full_text = record.get("full_text") or "" + + print(f"Precedent: {citation} — {record.get('case_name')}") + print(f" court={court} is_binding={is_binding} prompt={'BINDING' if is_binding else 'PERSUASIVE'}") + print(f" model={MODEL} reasoning={REASONING} concurrency={CONCURRENCY}") + print(f" codex_bin={CODEX_BIN}") + + # ---- Side A: existing stored halachot (current production / Opus output) ---- + existing = await db.list_halachot(case_law_id=case_law_id, limit=500) + by_status = Counter(h.get("review_status") for h in existing) + print(f"\n[A] existing halachot in DB: {len(existing)} status breakdown: {dict(by_status)}") + approved = by_status.get("approved", 0) + by_status.get("published", 0) + if approved: + print(f" (extracted by Claude Opus — a REAL re-run would DELETE the {approved} approved)") + + # ---- Side B: fresh extraction via codex / gpt-5.5 (no DB writes) ---- + chunks = await db.list_precedent_chunks(case_law_id, section_types=hx.EXTRACTABLE_SECTIONS) + if not chunks: + chunks = await db.list_precedent_chunks(case_law_id) + print(f"\n[B] extracting from {len(chunks)} chunks via codex/{MODEL} @ {REASONING} ...") + context = f"מקור: {citation} — {court}, {date_str}" + sem = asyncio.Semaphore(CONCURRENCY) + + async def bounded(i, c): + async with sem: + return await extract_chunk( + c["content"], c["section_type"], i, len(chunks), context, is_binding + ) + + results = await asyncio.gather(*[bounded(i, c) for i, c in enumerate(chunks)]) + raw_b, failed = [], 0 + for items, ok in results: + raw_b.extend(items) + if not ok: + failed += 1 + + cleaned_b = [] + for raw in raw_b: + coerced = hx._coerce_halacha(raw, is_binding=is_binding) + if coerced is None: + continue + coerced["quote_verified"] = hx._verify_quote(coerced["supporting_quote"], full_text) + cleaned_b.append(coerced) + + print(f" raw={len(raw_b)} valid={len(cleaned_b)} failed_chunks={failed}/{len(chunks)}") + + # ---- Comparison ---- + a_stats = stats(existing, f"A · Opus (production, n={len(existing)})") + b_stats = stats(cleaned_b, f"B · codex/{MODEL} @ {REASONING}") + print_stats(a_stats) + print_stats(b_stats) + + # Dump B halachot for human quality review + safe_citation = citation.replace("/", "_").replace('"', "").strip() + out_path = ( + f"/home/chaim/legal-ai/data/" + f"ab_halacha_codex_{safe_citation}_{MODEL}_{REASONING}.json" + ) + with open(out_path, "w", encoding="utf-8") as f: + json.dump( + { + "precedent": citation, + "model": MODEL, + "reasoning": REASONING, + "engine": "codex", + "A_stats": a_stats, + "B_stats": b_stats, + "B_halachot": cleaned_b, + }, + f, + ensure_ascii=False, + indent=2, + ) + print(f"\nB halachot written to: {out_path}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/adapter_profiles.py b/scripts/adapter_profiles.py index a61f0c5..9816523 100644 --- a/scripts/adapter_profiles.py +++ b/scripts/adapter_profiles.py @@ -6,8 +6,8 @@ flip. Each adapter family differs on three axes that, if not reconciled, crash the agent immediately or degrade it silently: 1. model id ↔ provider — each adapter expects a model from its own provider - (claude-* / gemini-* / deepseek-*). Wrong/foreign id fails or silently - falls back to the adapter default. + (claude-* / gemini-* / deepseek-* / OpenAI Codex-capable ids). Wrong or + foreign ids fail or silently fall back to the adapter default. 2. instructions transport — • file_path adapters pass the instructions file as a *path* (`claude --append-system-prompt-file`); the CLI parses it, @@ -27,8 +27,8 @@ Keeping these facts in one declarative registry makes "migrate any agent to any adapter" data-driven: adding a future adapter (codex_local, grok_local, ...) is one new entry here. Consumed by scripts/migrate_agent_adapter.py. -Verified 2026-06-13 against the installed adapter packages -(`@paperclipai/adapter-{claude,gemini}-local`, in-repo +Verified 2026-06-17 against the installed adapter packages +(`@paperclipai/adapter-{claude,gemini,codex}-local`, in-repo `adapters/deepseek-paperclip-adapter`) and the live Paperclip agents table. """ from __future__ import annotations @@ -60,6 +60,19 @@ ADAPTER_PROFILES: dict[str, dict] = { "frontmatter_safe": False, "tool_config": "hermes", }, + "codex_local": { + "provider": "codex", + "default_model": "gpt-5.5", + "instructions_mode": "content_arg", + "frontmatter_safe": False, + "tool_config": "codex_home", + "model_prefixes": ( + "gpt-", + "o3", + "o4", + "codex-", + ), + }, } @@ -89,5 +102,8 @@ def model_matches_provider(model: str, adapter_type: str) -> bool: default model, which is by definition in-family.""" if not model: return True - provider = get_profile(adapter_type)["provider"] - return model.startswith(provider) + profile = get_profile(adapter_type) + prefixes = profile.get("model_prefixes") + if prefixes: + return model.startswith(tuple(prefixes)) + return model.startswith(profile["provider"]) diff --git a/web-ui/src/components/operations/agent-adapters-panel.tsx b/web-ui/src/components/operations/agent-adapters-panel.tsx index 3d405ca..8752ee1 100644 --- a/web-ui/src/components/operations/agent-adapters-panel.tsx +++ b/web-ui/src/components/operations/agent-adapters-panel.tsx @@ -3,7 +3,7 @@ /** * מתאמי-סוכנים (Adapter) — /operations panel. * - * Migrate any committee agent between run-engines (Claude / Gemini / DeepSeek) + * Migrate any committee agent between run-engines (Claude / Gemini / DeepSeek / Codex) * in BOTH companies at once, with a preflight that prevents the silent crash * (frontmatter `---` breaks the gemini/deepseek CLI; model must match provider; * gemini excludeTools is global). Built to the Claude Design mockup @@ -57,6 +57,7 @@ const ADAPTERS = [ { value: "claude_local", label: "Claude", cls: "bg-info-bg text-info border-info/40" }, { value: "gemini_local", label: "Gemini", cls: "bg-warn-bg text-warn border-warn/40" }, { value: "deepseek_local", label: "DeepSeek", cls: "bg-gold-wash text-gold-deep border-gold/40" }, + { value: "codex_local", label: "Codex", cls: "bg-success-bg text-success border-success/40" }, ] as const; // Bilingual role subtitle under the agent name (mockup 02d) — display-only, @@ -149,7 +150,7 @@ export function AgentAdaptersPanel() {

- העברת כל סוכן בין מנועי-ההרצה (Claude · Gemini · DeepSeek), בשתי החברות יחד. כל מעבר + העברת כל סוכן בין מנועי-ההרצה (Claude · Gemini · DeepSeek · Codex), בשתי החברות יחד. כל מעבר עובר preflight שמונע קריסה. מעבר ל-Gemini/DeepSeek = fallback מופחת-איכות — להחזיר ל-Claude כשטוקני-Claude חוזרים.

@@ -159,7 +160,7 @@ export function AgentAdaptersPanel() {
מצב-חירום · טוקני-Claude
- כשנגמרים הטוקנים — להפיל את כל הצוות ל-Gemini בלחיצה, ולהחזיר כשחוזרים. + כשנגמרים הטוקנים — להפיל את כל הצוות ל-Gemini או ל-Codex בלחיצה, ולהחזיר כשחוזרים.
@@ -171,6 +172,11 @@ export function AgentAdaptersPanel() { onClick={() => openMigrate("all", "gemini_local", "העברת כל הסוכנים ל-Gemini ⚡")}> העבר הכל ל-Gemini ⚡ +
@@ -272,7 +278,7 @@ export function AgentAdaptersPanel() { {dlg?.body.action === "revert" ? "שחזור מדויק למתאם ולמודל שהיו לפני המעבר, בשתי החברות." - : "המעבר חל על שתי החברות (CMP + CMPA). Gemini/DeepSeek = fallback מופחת-איכות."} + : "המעבר חל על שתי החברות (CMP + CMPA). Gemini/DeepSeek/Codex = fallback מופחת-איכות."} diff --git a/web-ui/src/lib/api/operations.ts b/web-ui/src/lib/api/operations.ts index 8d8926e..a32d1fe 100644 --- a/web-ui/src/lib/api/operations.ts +++ b/web-ui/src/lib/api/operations.ts @@ -231,7 +231,7 @@ export function useResetAgentSession() { // ── Agent adapter migration ──────────────────────────────────────────────── // Migrate an agent (or "all") between run-engines (claude_local / gemini_local / -// deepseek_local) in BOTH companies. Host-side (runs scripts/migrate_agent_adapter.py +// deepseek_local / codex_local) in BOTH companies. Host-side (runs scripts/migrate_agent_adapter.py // via the court-fetch bridge), so the script's exit code + output are relayed so // the panel can render preflight warnings. A non-zero exit on a "check" is an // informative refusal, not a transport error — callers inspect exit_code.