From 69d4827f336b00b9df6321375c1220f54f8df8f2 Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 4 May 2026 18:59:20 +0000 Subject: [PATCH] =?UTF-8?q?feat(migration):=20enrich=20internal=20committe?= =?UTF-8?q?e=20entries=20=E2=80=94=20fix=20case=5Fnumber=20+=20metadata=20?= =?UTF-8?q?+=20halachot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - precedent_metadata_extractor: add case_number_clean extraction field - apply_to_record: overwrite_case_number param for one-time migration - internal_decisions: enrich_migrated_entries() — runs metadata then queues halachot - server: expose as internal_decision_enrich MCP tool Co-Authored-By: Claude Sonnet 4.6 --- mcp-server/src/legal_mcp/server.py | 15 +++++ .../legal_mcp/services/internal_decisions.py | 62 +++++++++++++++++++ .../services/precedent_metadata_extractor.py | 21 ++++++- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/mcp-server/src/legal_mcp/server.py b/mcp-server/src/legal_mcp/server.py index 626c659..7f0098d 100644 --- a/mcp-server/src/legal_mcp/server.py +++ b/mcp-server/src/legal_mcp/server.py @@ -624,6 +624,21 @@ async def internal_decision_migrate( return _json.dumps(results, ensure_ascii=False, indent=2) +@mcp.tool() +async def internal_decision_enrich( + dry_run: bool = True, +) -> str: + """העשרת החלטות שהומגרו (חד-פעמי): תיקון מספר ערר + שם + תאריך + תור להלכות. + + dry_run=True — מציג כמה רשומות יטופלו ללא כתיבה. + dry_run=False — מריץ בפועל: metadata extraction (תיקון case_number/case_name/date) ואחר כך תור חילוץ הלכות. + """ + import json as _json + from legal_mcp.services import internal_decisions as int_svc + result = await int_svc.enrich_migrated_entries(dry_run=dry_run) + return _json.dumps(result, ensure_ascii=False, indent=2) + + @mcp.tool() async def record_chair_feedback( case_number: str, diff --git a/mcp-server/src/legal_mcp/services/internal_decisions.py b/mcp-server/src/legal_mcp/services/internal_decisions.py index 195d944..2f8bf66 100644 --- a/mcp-server/src/legal_mcp/services/internal_decisions.py +++ b/mcp-server/src/legal_mcp/services/internal_decisions.py @@ -285,6 +285,68 @@ async def migrate_from_external_corpus(dry_run: bool = False) -> dict: return results +async def enrich_migrated_entries(dry_run: bool = False) -> dict: + """One-time enrichment: run metadata extraction + halacha extraction on all + internal_committee entries that are waiting (halacha_status='pending', + metadata never requested). + + Metadata extraction will: + - Fix case_number from the decision header text + - Fill case_name from the parties line + - Fill date if missing + + Halacha extraction queues the LLM-based halacha extraction job. + """ + from legal_mcp.services import precedent_metadata_extractor, db as _db + + pool = await _db.get_pool() + async with pool.acquire() as conn: + rows = await conn.fetch( + """SELECT id, case_number + FROM case_law + WHERE source_kind = 'internal_committee' + AND halacha_extraction_status = 'pending' + AND metadata_extraction_requested_at IS NULL + ORDER BY created_at""" + ) + + results = { + "total": len(rows), + "metadata_updated": 0, + "halachot_queued": 0, + "failed": 0, + "dry_run": dry_run, + } + + if dry_run: + return results + + for row in rows: + case_law_id = row["id"] + try: + meta = await precedent_metadata_extractor.extract_and_apply( + case_law_id, overwrite_case_number=True + ) + if meta.get("status") in ("completed", "no_changes"): + results["metadata_updated"] += 1 + logger.info( + "enrich_migrated: %s → fields=%s", + row["case_number"], meta.get("fields"), + ) + except Exception as e: + logger.error("enrich_migrated metadata failed for %s: %s", row["case_number"], e) + results["failed"] += 1 + continue + + try: + await _db.request_halacha_extraction(case_law_id) + results["halachot_queued"] += 1 + except Exception as e: + logger.error("enrich_migrated halacha queue failed for %s: %s", row["case_number"], e) + + return results + + async def search_internal( query: str, *, diff --git a/mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py b/mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py index 5ec7af9..2d844c6 100644 --- a/mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py +++ b/mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py @@ -50,7 +50,8 @@ METADATA_EXTRACTION_PROMPT = """אתה מסייע משפטי בכיר. קרא א "decision_date_iso": "YYYY-MM-DD — תאריך מתן ההחלטה כפי שמופיע בטקסט (בכותרת או בחתימה הסופית). אם לא ניתן לזהות במדויק — מחרוזת ריקה.", "precedent_level": "אחד מ-4: 'עליון' / 'מנהלי' / 'ועדת_ערר_ארצית' / 'ועדת_ערר_מחוזית'. בחר לפי הערכאה שמסומנת בכותרת הפסק. אם לא ברור — מחרוזת ריקה.", "source_type": "אחד מ-2: 'court_ruling' (פסק דין של בית משפט — עליון/מנהלי) / 'appeals_committee' (החלטה של ועדת ערר). אם לא ברור — מחרוזת ריקה.", - "court": "שם הערכאה כפי שהוא מופיע בכותרת (למשל 'בית המשפט העליון', 'בית המשפט המחוזי בירושלים בשבתו כבית משפט לעניינים מנהליים', 'ועדת הערר לתכנון ובניה פיצויים והיטלי השבחה — מחוז ירושלים'). מחרוזת ריקה אם לא ניתן לזהות." + "court": "שם הערכאה כפי שהוא מופיע בכותרת (למשל 'בית המשפט העליון', 'בית המשפט המחוזי בירושלים בשבתו כבית משפט לעניינים מנהליים', 'ועדת הערר לתכנון ובניה פיצויים והיטלי השבחה — מחוז ירושלים'). מחרוזת ריקה אם לא ניתן לזהות.", + "case_number_clean": "מספר הערר/תיק כפי שמופיע בכותרת — רק הספרות והאלכסון, למשל '1062/24' או '8031/21'. ללא המילה 'ערר', ללא שם הצדדים, ללא סוגריים. אם יש כמה עררים מאוחדים — הרשום הראשון. מחרוזת ריקה אם לא ניתן לזהות." } ## כללי איכות @@ -161,12 +162,15 @@ async def extract_metadata(case_law_id: UUID | str) -> dict: out["source_type"] = st if isinstance(result.get("court"), str): out["court"] = result["court"].strip() + if isinstance(result.get("case_number_clean"), str): + out["case_number_clean"] = result["case_number_clean"].strip() return out async def apply_to_record( case_law_id: UUID | str, suggested: dict, + overwrite_case_number: bool = False, ) -> dict: """Merge suggested metadata into the case_law row, filling ONLY empty fields. @@ -178,6 +182,9 @@ async def apply_to_record( case_name has special handling: if the current case_name equals the case_number (a tell-tale sign of the upload form sending the long citation into both fields), treat it as empty and overwrite. + + overwrite_case_number: when True, update case_number from case_number_clean + even if the field already has a value (used for one-time migration enrichment). """ if isinstance(case_law_id, str): case_law_id = UUID(case_law_id) @@ -250,6 +257,11 @@ async def apply_to_record( if c: fields_to_update["court"] = c + if overwrite_case_number: + cn = (suggested.get("case_number_clean") or "").strip() + if cn: + fields_to_update["case_number"] = cn + if not fields_to_update: return {"updated": False, "fields": []} @@ -257,12 +269,15 @@ async def apply_to_record( return {"updated": True, "fields": list(fields_to_update.keys())} -async def extract_and_apply(case_law_id: UUID | str) -> dict: +async def extract_and_apply( + case_law_id: UUID | str, + overwrite_case_number: bool = False, +) -> dict: """Convenience wrapper: extract → merge into row → return summary.""" suggested = await extract_metadata(case_law_id) if not suggested: return {"status": "no_metadata", "fields": []} - result = await apply_to_record(case_law_id, suggested) + result = await apply_to_record(case_law_id, suggested, overwrite_case_number=overwrite_case_number) return { "status": "completed" if result["updated"] else "no_changes", "fields": result["fields"],