המשך מיגרציית INV-TOOL1 מעבר למשפחת-החיפוש (#71). הומרו ל-{status,data,message}: precedent_library, citations, internal_decisions, missing_precedents, training_enrichment, precedents, legal_arguments, cases, documents, workflow (~55 כלים). בוטלו 5 עותקי _ok/_err משוכפלים (alias ל-tools/envelope.py — SSoT, G2). עיקרון: envelope-status = הצלחת-הקריאה-לכלי; תוצאה-עסקית (idempotent_existing, noop, completed...) נשמרת בתוך data. err רק לכשל אמיתי (not-found/invalid/exception). תאימות-API: צרכני web/app.py של cases/workflow/precedents חוּוטו דרך envelope_unwrap + בדיקת status=="error"→4xx — תשובת ה-HTTP זהה, web-ui לא מושפע. (documents/legal_arguments/citations/... אינם נצרכים מ-app.py — agent-only.) בדיקות: 182/182 עוברים (test_corpus_constraints עודכן לחוזה החדש). נותר: משפחת drafting (מסלול הפקת-ההחלטה) בפרוסה נפרדת עם שער טסט-ייצוא. Invariants: מקדם INV-TOOL1 + G2 (SSoT, ביטול כפילות). מתועד ב-X9 + gap-audit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
5.1 KiB
Python
128 lines
5.1 KiB
Python
"""MCP tools for the internal-decisions citation graph (TaskMaster #34).
|
||
|
||
The citation graph captures pointers between Daphna's (and other internal
|
||
committee chairs') decisions: when one ruling cites another, ``precedent_
|
||
internal_citations`` records the edge — resolved against ``case_law`` when
|
||
the cited row exists, kept as a stub when it doesn't.
|
||
|
||
Three tools:
|
||
|
||
- ``extract_internal_citations`` — run regex extraction on one row (by id) or
|
||
on every internal-committee row filtered by chair (e.g. Daphna only).
|
||
Idempotent: re-running does not duplicate rows (ON CONFLICT DO NOTHING).
|
||
- ``list_internal_citations`` — outgoing edges from a source row. Optional
|
||
``linked_only`` filter for rows resolved to existing case_law UUIDs.
|
||
- ``list_incoming_citations`` — incoming edges to a target row ("which
|
||
Daphna decisions cite this ruling?").
|
||
|
||
These tools are *manual triggers*. The pipeline runs them after a new
|
||
internal-decision upload, but the chair / researcher can also re-run on
|
||
demand (for example after fixing OCR or after uploading a previously-
|
||
missing decision so that newer rows now link to it).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from uuid import UUID
|
||
|
||
from legal_mcp.services import citation_extractor
|
||
from legal_mcp.tools.envelope import err as _err, ok as _ok # GAP-48: SSoT envelope
|
||
|
||
|
||
async def extract_internal_citations(
|
||
case_law_id: str = "",
|
||
chair_name: str = "",
|
||
limit: int = 0,
|
||
) -> str:
|
||
"""חילוץ ציטוטים פנימיים מהחלטות ועדת ערר ושמירה ב-precedent_internal_citations.
|
||
|
||
Args:
|
||
case_law_id: UUID של החלטה ספציפית. אם ריק וגם chair_name ריק — מריץ
|
||
על כל ההחלטות internal_committee. אם מסופק, חייב לעבור על שורה אחת
|
||
בלבד (משתמש בזה אחרי upload).
|
||
chair_name: שם יו"ר (כגון 'דפנה תמיר'). מסנן את האצווה. ריק = כל היו"רים.
|
||
limit: עליון על מספר רשומות שיעובדו (0 = ללא הגבלה). שימושי לבדיקה.
|
||
|
||
הכלי איידמפוטנטי — ON CONFLICT DO NOTHING על (source_case_law_id, cited_case_number).
|
||
מחזיר סטטיסטיקה: extracted, linked, new, skipped, failed.
|
||
"""
|
||
if case_law_id.strip() and chair_name.strip():
|
||
return _err("יש לספק case_law_id או chair_name, לא שניהם")
|
||
|
||
if case_law_id.strip():
|
||
try:
|
||
cl_uuid = UUID(case_law_id.strip())
|
||
except ValueError:
|
||
return _err("case_law_id לא תקין")
|
||
try:
|
||
stats = await citation_extractor.extract_and_store(cl_uuid)
|
||
except Exception as e:
|
||
return _err(str(e))
|
||
return _ok(stats)
|
||
|
||
try:
|
||
stats = await citation_extractor.extract_all_internal_committee(
|
||
chair_name_filter=chair_name.strip(),
|
||
limit=int(limit) if limit else 0,
|
||
)
|
||
except Exception as e:
|
||
return _err(str(e))
|
||
return _ok(stats)
|
||
|
||
|
||
async def list_internal_citations(
|
||
case_law_id: str = "",
|
||
linked_only: bool = False,
|
||
limit: int = 50,
|
||
) -> str:
|
||
"""רשימת ציטוטים יוצאים מהחלטה (מה ההחלטה הזו מצטטת).
|
||
|
||
Args:
|
||
case_law_id: UUID של ה-case_law (חובה).
|
||
linked_only: True = רק ציטוטים שקושרו ל-case_law קיים בקורפוס.
|
||
limit: עליון על מספר תוצאות (default 50).
|
||
|
||
Returns: JSON עם list של ציטוטים, כולל target_case_number/name/chair
|
||
כשהם linked. אם linked_only=False, ציטוטים בלתי קושרים יחזרו עם
|
||
cited_case_law_id=null וניתן להעלות אותם דרך internal_decision_upload.
|
||
"""
|
||
if not case_law_id.strip():
|
||
return _err("case_law_id חובה")
|
||
try:
|
||
cl_uuid = UUID(case_law_id.strip())
|
||
except ValueError:
|
||
return _err("case_law_id לא תקין")
|
||
try:
|
||
rows = await citation_extractor.list_citations_for_case_law(
|
||
cl_uuid, linked_only=bool(linked_only),
|
||
)
|
||
except Exception as e:
|
||
return _err(str(e))
|
||
return _ok({"items": rows[: max(1, int(limit))], "count": len(rows)})
|
||
|
||
|
||
async def list_incoming_citations(
|
||
case_law_id: str = "",
|
||
limit: int = 50,
|
||
) -> str:
|
||
"""רשימת ציטוטים נכנסים אל החלטה (אילו החלטות מצטטות אותה).
|
||
|
||
שימוש: רוצים לדעת אילו החלטות של דפנה הסתמכו על פסק דין מסוים?
|
||
מעבירים את ה-case_law_id של פסק הדין הזה.
|
||
|
||
Args:
|
||
case_law_id: UUID של ה-target case_law (חובה).
|
||
limit: עליון על מספר תוצאות.
|
||
"""
|
||
if not case_law_id.strip():
|
||
return _err("case_law_id חובה")
|
||
try:
|
||
cl_uuid = UUID(case_law_id.strip())
|
||
except ValueError:
|
||
return _err("case_law_id לא תקין")
|
||
try:
|
||
rows = await citation_extractor.list_citations_to_case_law(cl_uuid)
|
||
except Exception as e:
|
||
return _err(str(e))
|
||
return _ok({"items": rows[: max(1, int(limit))], "count": len(rows)})
|