Merge pull request 'feat(learning): כל החלטה שלנו תמיד בספריית-הפסיקה + בדיקת-ציטוטים וסימון-חסרים אוטומטי' (#163) from worktree-final-into-library into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 40s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 40s
This commit was merged in pull request #163.
This commit is contained in:
@@ -53,7 +53,12 @@
|
|||||||
*מקורות:* quality-at-source (Data Mesh) · separation-of-concerns. *סטטוס: verified.*
|
*מקורות:* quality-at-source (Data Mesh) · separation-of-concerns. *סטטוס: verified.*
|
||||||
|
|
||||||
### 0.6 מסלול-העלאת-סופי נקי + פאנלים אוטומטיים (מדורג)
|
### 0.6 מסלול-העלאת-סופי נקי + פאנלים אוטומטיים (מדורג)
|
||||||
היו"ר מעלה את **ההחלטה החתומה שלה** דרך מסלול ייעודי — `POST /api/cases/{case}/final/upload` (כפתור "העלאת החלטה סופית של היו"ר" בלשונית-הטיוטות). **נבדל** מ-`exports/upload` (גרסה-מתוקנת-שלנו+retrofit) ומ-`mark-final` (סימון export-שלנו), ולכן אינו מסלול-מקביל (G2) אלא יכולת חסרה: קליטת final חיצוני, פתיחת `draft_final_pairs` (`final_received`), והכנסה לקורפוס-הסגנון תחת ה-`case_number` **המלא** (בל"מ≠ערר — מונע התנגשות-מספר).
|
היו"ר מעלה את **ההחלטה החתומה שלה** דרך מסלול ייעודי — `POST /api/cases/{case}/final/upload` (כפתור "העלאת החלטה סופית של היו"ר" בלשונית-הטיוטות). **נבדל** מ-`exports/upload` (גרסה-מתוקנת-שלנו+retrofit) ומ-`mark-final` (סימון export-שלנו), ולכן אינו מסלול-מקביל (G2) אלא יכולת חסרה.
|
||||||
|
הקליטה (סינכרונית ב-endpoint) מבצעת את **לולאת-צמיחת-הקורפוס** (§1.3) במלואה:
|
||||||
|
1. **קורפוס-הסגנון** (voice) תחת ה-`case_number` **המלא** (בל"מ≠ערר — מונע התנגשות-מספר) + פתיחת `draft_final_pairs` (`final_received`, INV-LRN4).
|
||||||
|
2. **ספריית-הפסיקה** — ההחלטה נכנסת ל-`case_law` כ-`internal_committee` **תמיד** (כדי שתהיה ברת-ציטוט בהחלטות עתידיות). `chair_name` נקבע **דטרמיניסטית** (תיק → ברירת-מחדל-ועדה, לעולם לא ריק — אילוץ `case_law_internal_chair_check`); לא נשען על חילוץ-LLM. מטה-דאטה נוסף (תאריך/צדדים) מועשר אסינכרונית ע"י מחלץ-Gemini.
|
||||||
|
3. **בדיקת-ציטוטים** — `extract_internal_citations` מקשר את הפסיקה שההחלטה מצטטת לספרייה; כל ציטוט שאינו בספרייה **מסומן אוטומטית** כ-`missing_precedent` (open) להעלאה ע"י היו"ר.
|
||||||
|
4. הציטוטים-המקושרים מזינים את **לולאת-ה-corroboration** (X11): ציטוט-נכנס מההחלטה שלנו מחזק את ההלכות של התקדים המצוטט (`corroboration_rebuild`).
|
||||||
ואז שני שלבים אוטומטיים נפרדים (`run-learning` / `run-halacha`) המעירים worker מקומי (claude/DeepSeek/Gemini מקומיים בלבד):
|
ואז שני שלבים אוטומטיים נפרדים (`run-learning` / `run-halacha`) המעירים worker מקומי (claude/DeepSeek/Gemini מקומיים בלבד):
|
||||||
- **למידה:** `ingest_final_version` (Opus distillation) → **פאנל-סגנון דו-סוכני** (DeepSeek+Gemini, "למידה כפולה") שמצביע על כל לקח-style_method; הסכמה 2/2 → `decision_lesson` (`source=panel:deepseek+gemini`); פיצול → ליו"ר.
|
- **למידה:** `ingest_final_version` (Opus distillation) → **פאנל-סגנון דו-סוכני** (DeepSeek+Gemini, "למידה כפולה") שמצביע על כל לקח-style_method; הסכמה 2/2 → `decision_lesson` (`source=panel:deepseek+gemini`); פיצול → ליו"ר.
|
||||||
- **הלכות:** `extract_internal_citations` → `precedent_extract_halachot` → `corroboration_rebuild` → **פאנל-הלכות תלת-סוכני** (`halacha_panel_approve.py --apply`).
|
- **הלכות:** `extract_internal_citations` → `precedent_extract_halachot` → `corroboration_rebuild` → **פאנל-הלכות תלת-סוכני** (`halacha_panel_approve.py --apply`).
|
||||||
|
|||||||
@@ -166,6 +166,15 @@ export function DraftsPanel({
|
|||||||
toast.success(
|
toast.success(
|
||||||
`ההחלטה הסופית נקלטה — ${data.final_words} מילים (לעומת ${data.draft_words} בטיוטה)`,
|
`ההחלטה הסופית נקלטה — ${data.final_words} מילים (לעומת ${data.draft_words} בטיוטה)`,
|
||||||
);
|
);
|
||||||
|
const lib = data.library;
|
||||||
|
if (lib?.enrolled) {
|
||||||
|
toast.success(
|
||||||
|
`נוספה לספריית-הפסיקה · ${lib.linked ?? 0} ציטוטים קושרו · ` +
|
||||||
|
`${lib.missing_flagged ?? 0} חסרים סומנו להעלאה`,
|
||||||
|
);
|
||||||
|
} else if (lib?.error) {
|
||||||
|
toast.warning(`לא נוספה לספריית-הפסיקה: ${lib.error}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (err) =>
|
onError: (err) =>
|
||||||
toast.error(err instanceof Error ? err.message : "שגיאה בהעלאה"),
|
toast.error(err instanceof Error ? err.message : "שגיאה בהעלאה"),
|
||||||
|
|||||||
@@ -189,6 +189,15 @@ export type FinalUploadResult = {
|
|||||||
pair_id: string | null;
|
pair_id: string | null;
|
||||||
draft_words: number;
|
draft_words: number;
|
||||||
final_words: number;
|
final_words: number;
|
||||||
|
chair_name?: string;
|
||||||
|
library?: {
|
||||||
|
enrolled: boolean;
|
||||||
|
case_law_id?: string | null;
|
||||||
|
linked?: number;
|
||||||
|
missing_flagged?: number;
|
||||||
|
error?: string;
|
||||||
|
citation_error?: string;
|
||||||
|
};
|
||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
119
web/app.py
119
web/app.py
@@ -3312,6 +3312,96 @@ async def api_mark_final(case_number: str, filename: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Our committee's standing chair — internal_committee rows REQUIRE a chair_name
|
||||||
|
# (DB constraint case_law_internal_chair_check). Both CMP (1xxx) and CMPA (8/9xxx)
|
||||||
|
# are currently chaired by Dafna Tamir; map by prefix so adding a chair later is local.
|
||||||
|
COMMITTEE_CHAIR_BY_PREFIX = {"1": "דפנה תמיר", "8": "דפנה תמיר", "9": "דפנה תמיר"}
|
||||||
|
COMMITTEE_CHAIR_DEFAULT = "דפנה תמיר"
|
||||||
|
|
||||||
|
|
||||||
|
def _committee_chair_for_case(case: dict, case_number: str) -> str:
|
||||||
|
"""Resolve the chair for one of OUR decisions deterministically (no LLM): the case's
|
||||||
|
own chair_name, else the committee default by case-number prefix."""
|
||||||
|
existing = (case.get("chair_name") or "").strip()
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
return COMMITTEE_CHAIR_BY_PREFIX.get(case_number[:1], COMMITTEE_CHAIR_DEFAULT)
|
||||||
|
|
||||||
|
|
||||||
|
async def _enroll_final_in_library(
|
||||||
|
case: dict, case_number: str, final_text: str, chair_name: str,
|
||||||
|
) -> dict:
|
||||||
|
"""לולאת-צמיחת-הקורפוס (07-learning §1.3): GUARANTEE the signed final enters the
|
||||||
|
precedent library (case_law, internal_committee) so it is citable; extract the
|
||||||
|
precedents IT cites; and auto-flag any cited precedent missing from the library.
|
||||||
|
|
||||||
|
Surfaces failures in the return value (no silent swallow) — the upload itself still
|
||||||
|
succeeds (file + style_corpus + pair are already saved)."""
|
||||||
|
from legal_mcp.services import internal_decisions as int_svc
|
||||||
|
from legal_mcp.tools import citations as cit_tools
|
||||||
|
from legal_mcp.tools import missing_precedents as mp_tools
|
||||||
|
|
||||||
|
out: dict = {"enrolled": False, "linked": 0, "missing_flagged": 0}
|
||||||
|
if not final_text.strip():
|
||||||
|
out["error"] = "no final text extracted"
|
||||||
|
return out
|
||||||
|
try:
|
||||||
|
res = await int_svc.ingest_internal_decision(
|
||||||
|
case_number=case_number, case_name=case.get("title", ""),
|
||||||
|
decision_date=case.get("decision_date"), chair_name=chair_name,
|
||||||
|
district="ירושלים", practice_area=case.get("practice_area", ""),
|
||||||
|
appeal_subtype=case.get("appeal_subtype", ""), text=final_text,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("library enrollment failed for %s: %s", case_number, e)
|
||||||
|
out["error"] = str(e)
|
||||||
|
return out
|
||||||
|
case_law_id = res.get("case_law_id") if isinstance(res, dict) else None
|
||||||
|
out["enrolled"] = bool(case_law_id)
|
||||||
|
out["case_law_id"] = case_law_id
|
||||||
|
if not case_law_id:
|
||||||
|
return out
|
||||||
|
|
||||||
|
# The precedents this decision cites → link to the library; flag the ones not found.
|
||||||
|
try:
|
||||||
|
await cit_tools.extract_internal_citations(case_law_id=case_law_id, limit=0)
|
||||||
|
pool = await db.get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
out["linked"] = await conn.fetchval(
|
||||||
|
"SELECT count(*) FROM precedent_internal_citations "
|
||||||
|
"WHERE source_case_law_id = $1 AND cited_case_law_id IS NOT NULL",
|
||||||
|
UUID(case_law_id),
|
||||||
|
)
|
||||||
|
unresolved = await conn.fetch(
|
||||||
|
"SELECT DISTINCT cited_case_number FROM precedent_internal_citations "
|
||||||
|
"WHERE source_case_law_id = $1 AND cited_case_law_id IS NULL "
|
||||||
|
"AND cited_case_number <> ''",
|
||||||
|
UUID(case_law_id),
|
||||||
|
)
|
||||||
|
already = {r["citation"] for r in await conn.fetch(
|
||||||
|
"SELECT citation FROM missing_precedents WHERE status = 'open'")}
|
||||||
|
flagged = 0
|
||||||
|
for r in unresolved:
|
||||||
|
cit = r["cited_case_number"]
|
||||||
|
if cit in already:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
await mp_tools.missing_precedent_create(
|
||||||
|
citation=cit, case_number=case_number, cited_by_party="committee",
|
||||||
|
notes=f"מצוטט בהחלטת {case_number}; חסר בספריית-הפסיקה "
|
||||||
|
f"(זוהה אוטומטית בקליטת הסופי)",
|
||||||
|
)
|
||||||
|
already.add(cit)
|
||||||
|
flagged += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("missing_precedent_create failed for %s: %s", cit, e)
|
||||||
|
out["missing_flagged"] = flagged
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("citation check failed for %s: %s", case_number, e)
|
||||||
|
out["citation_error"] = str(e)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/cases/{case_number}/final/upload")
|
@app.post("/api/cases/{case_number}/final/upload")
|
||||||
async def api_upload_final_decision(case_number: str, file: UploadFile = File(...)):
|
async def api_upload_final_decision(case_number: str, file: UploadFile = File(...)):
|
||||||
"""Clean path: upload the CHAIR's signed final decision (Dafna's version).
|
"""Clean path: upload the CHAIR's signed final decision (Dafna's version).
|
||||||
@@ -3321,10 +3411,16 @@ async def api_upload_final_decision(case_number: str, file: UploadFile = File(..
|
|||||||
becomes active_draft). NOT for the chair's final.
|
becomes active_draft). NOT for the chair's final.
|
||||||
• exports/{f}/mark-final → marks one of *our* exports as final.
|
• exports/{f}/mark-final → marks one of *our* exports as final.
|
||||||
|
|
||||||
This endpoint takes the EXTERNAL signed final, stores it canonically, enrolls it in
|
This endpoint takes the EXTERNAL signed final, stores it canonically, and wires it
|
||||||
the style corpus, and opens the draft↔final reconciliation pair (INV-LRN4) so the
|
into the full corpus-growth loop (07-learning §1.3):
|
||||||
staged learning step can persist its analysis. It does NOT touch active_draft and
|
• style corpus (voice learning) + draft↔final pair (INV-LRN4);
|
||||||
does NOT run the LLM pipeline (run-learning / run-halacha do, on the local worker).
|
• the precedent library (case_law, internal_committee) so the decision is CITABLE —
|
||||||
|
chair_name is resolved deterministically (case → committee default), never left
|
||||||
|
empty (DB constraint), so enrollment always succeeds;
|
||||||
|
• its citations are linked to the library and any cited precedent NOT in the library
|
||||||
|
is auto-flagged as a missing_precedent.
|
||||||
|
It does NOT touch active_draft. The LLM-heavy steps (style/halacha panels, halacha
|
||||||
|
extraction, corroboration) still run on the local worker via run-learning/run-halacha.
|
||||||
"""
|
"""
|
||||||
case = await db.get_case_by_number(case_number)
|
case = await db.get_case_by_number(case_number)
|
||||||
if not case:
|
if not case:
|
||||||
@@ -3356,12 +3452,15 @@ async def api_upload_final_decision(case_number: str, file: UploadFile = File(..
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("final text extraction failed for %s: %s", case_number, e)
|
logger.warning("final text extraction failed for %s: %s", case_number, e)
|
||||||
|
|
||||||
# Case → final.
|
# Case → final + ensure chair_name is set (our committee chair; never empty).
|
||||||
|
chair_name = _committee_chair_for_case(case, case_number)
|
||||||
pool = await db.get_pool()
|
pool = await db.get_pool()
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"UPDATE cases SET status = 'final', updated_at = now() WHERE id = $1",
|
"UPDATE cases SET status = 'final', "
|
||||||
UUID(case["id"]),
|
"chair_name = COALESCE(NULLIF(chair_name, ''), $2), updated_at = now() "
|
||||||
|
"WHERE id = $1",
|
||||||
|
UUID(case["id"]), chair_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# INV-LRN4 — snapshot OUR draft now and open the pair (status=final_received).
|
# INV-LRN4 — snapshot OUR draft now and open the pair (status=final_received).
|
||||||
@@ -3385,6 +3484,10 @@ async def api_upload_final_decision(case_number: str, file: UploadFile = File(..
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("draft_final_pair snapshot failed for %s: %s", case_number, e)
|
logger.warning("draft_final_pair snapshot failed for %s: %s", case_number, e)
|
||||||
|
|
||||||
|
# Corpus-growth loop: guarantee enrollment into the precedent library + citation
|
||||||
|
# checking + missing-precedent flagging (07-learning §1.3). Surfaced, not silent.
|
||||||
|
library = await _enroll_final_in_library(case, case_number, final_text, chair_name)
|
||||||
|
|
||||||
case_dir = config.find_case_dir(case_number)
|
case_dir = config.find_case_dir(case_number)
|
||||||
if case_dir.exists():
|
if case_dir.exists():
|
||||||
commit_and_push(case_dir, f"החלטה סופית של היו\"ר: {final_name}")
|
commit_and_push(case_dir, f"החלטה סופית של היו\"ר: {final_name}")
|
||||||
@@ -3395,6 +3498,8 @@ async def api_upload_final_decision(case_number: str, file: UploadFile = File(..
|
|||||||
"pair_id": pair_id,
|
"pair_id": pair_id,
|
||||||
"draft_words": draft_words,
|
"draft_words": draft_words,
|
||||||
"final_words": len(final_text.split()),
|
"final_words": len(final_text.split()),
|
||||||
|
"chair_name": chair_name,
|
||||||
|
"library": library,
|
||||||
"status": "final",
|
"status": "final",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user