diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 0a43f96..cf3559f 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -53,6 +53,7 @@ | `style_lesson_panel.py` | python | **פאנל-סגנון דו-סוכני (למידה כפולה).** על-גבי דיסטילציית-ה-Opus (draft↔final ב-`draft_final_pairs.analysis`), שני שופטים בלתי-תלויים — DeepSeek + Gemini-2.5-flash — מצביעים לכל לקח על השאלה הגסה "האם זו הנחיית-סגנון מופשטת ובת-הכללה (INV-LRN5 — קול ולא מהות)?". הסכמה 2/2-keep → נכתב כ-`decision_lesson` (`source=panel:deepseek+gemini`); 2/2-drop → לא נכתב; פיצול/substance → מוסלם ליו"ר. `--apply` הפיך, מגבה ל-`data/audit/`. הטמעה ל-SKILL.md/lessons.md נשארת שער-יו"ר ידני (INV-G10). מפתחות כמו פאנל-ההלכות. **חובה מקומי**. `--case ` / `--pair-id `. | שלב-למידה במסלול-הסופי | | `final_learning_pipeline.py` | python | **תזמור שלב-הלמידה (פקודה אחת).** מופעל ע"י הרמס כשלוחצים "הרץ למידת-קול" במסלול-הסופי. דטרמיניסטי: (1) `ingest_final_version` עם נתיב-הסופי, (2) רישום לקורפוס-הסגנון (idempotent), (3) `style_lesson_panel --apply`. מקפל את הזרימה לפקודה אחת כדי שהסוכן לא ירכיב כמה קריאות (חסין). idempotent. **חובה מקומי**. `--case `. | אוטו (כפתור run-learning) / ידני | | `final_halacha_pipeline.py` | python | **תזמור שלב-אימות-ההלכות (פקודה אחת).** מופעל ע"י הרמס כשלוחצים "הרץ אימות-הלכות". דטרמיניסטי: (1) `extract_internal_citations(chair)`, (2) `corroboration.build_all()`, (3) `halacha_panel_approve --apply`. **חובה מקומי**. `--case ` / `--limit N` (תקרת תור). | אוטו (כפתור run-halacha) / ידני | +| `curator_apply_pipeline_branch.py` | python | **מקור-אמת לחיווט-הכפתורים של הרמס.** prompt-ה-curator חי רק ב-Paperclip DB (`agents.adapter_config.promptTemplate`). הסקריפט מקדים branch כך שיקיצה עם reason `final_learning_*`/`final_halacha_*` מריצה את ה-pipeline המתאים (HOME/DOTENV/DATA_DIR מוחלטים → DeepSeek+Gemini keys + DATA_DIR נפתרים נכון) ועוצרת, אחרת §A/§B כרגיל. idempotent (מסיר branch קודם). מחיל על שני הסוכנים (CMP+CMPA). `--verify`. **להריץ אחרי reset/יצירה-מחדש של סוכן-curator.** | אחרי reset prompt של curator | | `halacha_panel_audit.py` | python | **רשת-ביטחון לפאנל** (selective-prediction monitoring) — דוגם הלכות שאושרו ע"י הפאנל (`reviewer LIKE 'panel:%'`), מריץ עליהן **שוב** את הצבעת-ה-KEEP של 3 השופטים, ומציף כל מקרה שכעת נוטה DROP (false-keep פוטנציאלי). report-only כברירת-מחדל; `--flag` מחזיר את ה-flips ל-`pending_review` לסקירת-יו"ר. `--sample N`/`--seed`. בסיס 2026-06-07: 0/15. מיועד להרצה תקופתית (שבועי). מייבא שופטים מ-`halacha_panel_approve`. **חובה מקומי**. | תקופתי (שבועי) — ניטור | | `halacha_panel_calibrate.py` | python | **כיול מדיניות-ההצבעה של הפאנל** (Trust-or-Escalate, ICLR 2025). מריץ את שאלת-ה-KEEP של `halacha_panel_approve` על מדגם-הזהב ומודד מול `is_holding` (הציר-הגס) precision+coverage לכל מדיניות (unanimous/majority) + ספירת false-keep/false-drop. נותן את **אחוז-הטעות בפועל** לבחירת סף-סיכון α. מייבא שופטים מ-`halacha_panel_approve` (מקור-אמת יחיד). read-only, **חובה מקומי**. | ידני — לפני חיווט `--apply` | | `halacha_rule_role_backfill.py` | python | **INV-DM7** — backfill חד-פעמי: מסווג-מחדש את ההלכות הישנות (`rule_type IN ('binding','persuasive')` — ערכי-סמכות שנשמרו במסווה תפקיד לפני פיצול הצירים) לאחד מחמשת **תפקידי-הכלל** (holding/interpretive/procedural/application/obiter) דרך claude_session המקומי (אפס עלות). **לא נוגע בסמכות** (נגזרת מ-`precedent_level`). `--apply` (ברירת-מחדל dry-run) / `--limit N` / `--concurrency`. כותב backup CSV ל-`data/audit/` תחילה. fail-safe (פריט שנכשל → נשמר ערך ישן). **חובה מקומי** (claude_session). | ידני חד-פעמי אחרי deploy של פיצול-הסמכות | diff --git a/scripts/curator_apply_pipeline_branch.py b/scripts/curator_apply_pipeline_branch.py new file mode 100644 index 0000000..ffe2b25 --- /dev/null +++ b/scripts/curator_apply_pipeline_branch.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Reproducible source-of-truth for the curator's PIPELINE-WAKE branch. + +The Hermes curator's prompt lives ONLY in the Paperclip DB +(agents.adapter_config.promptTemplate), not in a repo file. This script (re-)prepends +a branch so that a wake whose reason starts with `final_learning_` / `final_halacha_` +runs the matching local pipeline script and stops — instead of the default §A/§B +style-curation routine. Without it, clicking the run-learning / run-halacha buttons +wakes the curator but it does style-curation, never the pipeline. + +Idempotent: strips any prior branch (by the ORIG_START marker) and re-prepends the +current one. Applies to BOTH company curators (CMP + CMPA) so cross-company sync stays +consistent. Re-run this after any curator-prompt reset / agent re-creation. + + python3 scripts/curator_apply_pipeline_branch.py # apply + python3 scripts/curator_apply_pipeline_branch.py --verify # show head, no write + +Env knobs (defaults shown): + PAPERCLIP_DB_URL=postgresql://paperclip:paperclip@localhost:54329/paperclip +""" +from __future__ import annotations + +import argparse +import asyncio +import os + +import asyncpg + +PAPERCLIP_DB = os.environ.get( + "PAPERCLIP_DB_URL", "postgresql://paperclip:paperclip@localhost:54329/paperclip" +) +CURATOR_AGENTS = [ + "60dce831-5c5b-4bae-bda9-5282d506f0dc", # CMP (1xxx — licensing) + "d6f7c55d-570a-46b8-8d72-1286d07da0d8", # CMPA (8xxx/9xxx — betterment) +] +MARKER = "### PIPELINE-WAKE BRANCH (auto)" +ORIG_START = "אתה מנהל ידע" # first line of the canonical curator prompt + +# HOME=/home/chaim is the root fix: the Hermes agent runs with $HOME pointing at its +# profile dir, so Path.home() / expanduser('~') would resolve DATA_DIR and the +# DeepSeek/Gemini key files wrong. Pinning HOME (+ DOTENV_PATH/DATA_DIR) makes the +# pipeline resolve the case files AND both judge keys correctly. +BRANCH = MARKER + r''' +לפני כל דבר אחר — בדוק אם זו יקיצת-pipeline אוטומטית. הרץ בדיוק את הבלוק הבא: +```bash +WAKE="{{wakeReason}}" +case "$WAKE" in + final_learning_*|final_halacha_*) + KIND=$(printf '%s' "$WAKE" | cut -d_ -f2) + CASE="${WAKE#final_${KIND}_}" + cd /home/chaim/legal-ai/mcp-server && \ + HOME=/home/chaim DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data \ + nohup .venv/bin/python ../scripts/final_${KIND}_pipeline.py --case "$CASE" \ + > "/tmp/final_${KIND}_${CASE}.log" 2>&1 & + sleep 2 + echo "PIPELINE_STARTED final_${KIND}_pipeline case=$CASE log=/tmp/final_${KIND}_${CASE}.log" + ;; + *) echo "NO_PIPELINE_WAKE" ;; +esac +``` +אם הפלט הוא `PIPELINE_STARTED ...` — **זו כל המשימה**: כתוב comment קצר בעברית ("הופעל צינור לתיק ; התוצאות יופיעו ב-/training (סגנון) או /approvals + /precedents (הלכות) תוך מספר דקות."), סגור את ה-issue (status=done), ו**סיים מיד — אל תמשיך לסעיפים שלמטה**. +אם הפלט הוא `NO_PIPELINE_WAKE` — המשך כרגיל לתבנית שלמטה. + +--- + +''' + + +async def main(verify: bool) -> int: + conn = await asyncpg.connect(PAPERCLIP_DB) + try: + for aid in CURATOR_AGENTS: + row = await conn.fetchrow( + "SELECT name, adapter_config->>'promptTemplate' AS t FROM agents WHERE id=$1", + aid, + ) + if not row or not row["t"]: + print(f"{aid}: NO TEMPLATE — skip") + continue + t = row["t"] + if verify: + has = MARKER in t + print(f"{aid} ({row['name']}): branch={'present' if has else 'MISSING'} " + f"len={len(t)}") + continue + i = t.find(ORIG_START) + if i < 0: + print(f"{aid}: ORIG_START not found — skip (manual check)") + continue + new = BRANCH + t[i:] # strip any prior branch, re-prepend + await conn.execute( + "UPDATE agents SET adapter_config = " + "jsonb_set(adapter_config,'{promptTemplate}', to_jsonb($2::text)), " + "updated_at = now() WHERE id=$1", + aid, new, + ) + print(f"{aid} ({row['name']}): branch applied; template now {len(new)} chars") + finally: + await conn.close() + return 0 + + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--verify", action="store_true", help="report only, no write") + raise SystemExit(asyncio.run(main(ap.parse_args().verify))) diff --git a/web-ui/src/components/cases/drafts-panel.tsx b/web-ui/src/components/cases/drafts-panel.tsx index 8197e49..a3c9fe7 100644 --- a/web-ui/src/components/cases/drafts-panel.tsx +++ b/web-ui/src/components/cases/drafts-panel.tsx @@ -33,6 +33,7 @@ import { BLOCK_LABELS, type FeedbackCategory, } from "@/lib/api/feedback"; +import { useCaseCitations } from "@/lib/api/citations"; import type { CaseStatus } from "@/lib/api/cases"; import { toast } from "sonner"; import { @@ -47,6 +48,8 @@ import { Brain, Scale, Stamp, + Link2, + AlertTriangle, } from "lucide-react"; /* Statuses at which a draft is considered ready */ @@ -336,6 +339,9 @@ export function DraftsPanel({ )} + {/* ── Precedents cited inside the signed decision ── */} + + {/* ── Exports list ── */}
@@ -587,6 +593,69 @@ export function DraftsPanel({ ); } +/* ── Precedents cited inside the decision ──────────── */ + +function CitationsSection({ caseNumber }: { caseNumber: string }) { + const { data, isLoading } = useCaseCitations(caseNumber); + + // Nothing to show until the signed decision is in the precedent library. + if (isLoading || !data?.in_library) return null; + if (!data.linked.length && !data.missing.length) return null; + + return ( +
+
+ +

פסיקה שצוטטה בהחלטה

+ + {data.linked.length} בספרייה · {data.missing.length} חסרות + +
+ +
+ {data.linked.map((c) => ( + + + {c.citation} + {c.case_name && ( + — {c.case_name} + )} + + בספרייה + + + ))} + {data.missing.map((c) => ( +
+ + {c.citation} + + {c.flagged ? "חסרה — סומנה להעלאה" : "חסרה בספרייה"} + +
+ ))} +
+

+ פסיקה מקושרת מחזקת את ההלכות שלה (corroboration); חסרות מסומנות אוטומטית + להעלאה לספרייה. +

+
+ ); +} + /* ── New feedback dialog (case-scoped) ─────────────── */ function NewCaseFeedbackDialog({ caseNumber }: { caseNumber: string }) { diff --git a/web-ui/src/lib/api/citations.ts b/web-ui/src/lib/api/citations.ts new file mode 100644 index 0000000..7c4c3ea --- /dev/null +++ b/web-ui/src/lib/api/citations.ts @@ -0,0 +1,37 @@ +/** + * Citations domain — precedents CITED inside a case's signed decision. + * Powers the case-page "פסיקה שצוטטה בהחלטה" panel. + */ + +import { useQuery } from "@tanstack/react-query"; +import { apiRequest } from "./client"; + +export type LinkedCitation = { + citation: string; + cited_id: string; + case_name: string; + court: string; + precedent_level: string; +}; + +export type MissingCitation = { + citation: string; + flagged: boolean; +}; + +export type CaseCitations = { + in_library: boolean; + case_law_id?: string; + linked: LinkedCitation[]; + missing: MissingCitation[]; +}; + +export function useCaseCitations(caseNumber: string | undefined) { + return useQuery({ + queryKey: ["case-citations", caseNumber ?? ""], + queryFn: ({ signal }) => + apiRequest(`/api/cases/${caseNumber}/citations`, { signal }), + enabled: Boolean(caseNumber), + staleTime: 10_000, + }); +} diff --git a/web/app.py b/web/app.py index cc20d91..747adc2 100644 --- a/web/app.py +++ b/web/app.py @@ -3222,6 +3222,65 @@ async def api_get_active_draft(case_number: str): } +@app.get("/api/cases/{case_number}/citations") +async def api_case_citations(case_number: str): + """Precedents CITED inside this case's signed decision — split into those linked to + the precedent library and those still missing from it (flagged for upload). + + Powers the case-page "פסיקה שצוטטה בהחלטה" panel. Source: the decision's row in + case_law (source_kind='internal_committee') → precedent_internal_citations.""" + case = await db.get_case_by_number(case_number) + if not case: + raise HTTPException(404, f"תיק {case_number} לא נמצא") + pool = await db.get_pool() + async with pool.acquire() as conn: + law_id = await conn.fetchval( + "SELECT id FROM case_law WHERE case_number = $1 " + "AND source_kind = 'internal_committee' ORDER BY created_at DESC LIMIT 1", + case_number, + ) + if not law_id: + # decision not yet in the library (e.g. final not uploaded) + return {"in_library": False, "linked": [], "missing": []} + linked = await conn.fetch( + "SELECT pic.cited_case_number, cl.id AS cited_id, cl.case_name, cl.court, " + " cl.precedent_level " + "FROM precedent_internal_citations pic " + "JOIN case_law cl ON cl.id = pic.cited_case_law_id " + "WHERE pic.source_case_law_id = $1 " + "ORDER BY pic.cited_case_number", + law_id, + ) + missing = await conn.fetch( + "SELECT DISTINCT pic.cited_case_number, " + " EXISTS(SELECT 1 FROM missing_precedents mp " + " WHERE mp.citation = pic.cited_case_number AND mp.status = 'open') AS flagged " + "FROM precedent_internal_citations pic " + "WHERE pic.source_case_law_id = $1 AND pic.cited_case_law_id IS NULL " + " AND pic.cited_case_number <> '' " + "ORDER BY pic.cited_case_number", + law_id, + ) + return { + "in_library": True, + "case_law_id": str(law_id), + "linked": [ + { + "citation": r["cited_case_number"], + "cited_id": str(r["cited_id"]), + "case_name": r["case_name"] or "", + "court": r["court"] or "", + "precedent_level": r["precedent_level"] or "", + } + for r in linked + ], + "missing": [ + {"citation": r["cited_case_number"], "flagged": r["flagged"]} + for r in missing + ], + } + + @app.post("/api/cases/{case_number}/exports/{filename}/mark-final") async def api_mark_final(case_number: str, filename: str): """Mark an export as the final version — copies to training corpus."""