המשך מיגרציית 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>
203 lines
7.1 KiB
Python
203 lines
7.1 KiB
Python
"""MCP tools for the missing-precedents log.
|
|
|
|
When a researcher (or chair) finds a citation in a party brief that
|
|
isn't yet in the precedent_library, they record it here so:
|
|
|
|
1. The gap is visible in the UI (the chair can see all open citations
|
|
that need to be uploaded).
|
|
2. The writer agent doesn't try to use a precedent that isn't in the
|
|
corpus — it knows the gap is being tracked.
|
|
3. The chair has a clean closing workflow: upload the actual decision
|
|
via the precedent library / internal-decisions, then link it here.
|
|
|
|
Three tools:
|
|
- ``missing_precedent_create`` — log a new gap (researcher / chair).
|
|
- ``missing_precedent_list`` — list open gaps (optionally filtered).
|
|
- ``missing_precedent_close`` — close a gap (chair workflow).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from legal_mcp.services import db
|
|
from legal_mcp.tools.envelope import err as _err, ok as _ok # GAP-48: SSoT envelope
|
|
|
|
|
|
async def _resolve_case_id(case_number: str) -> UUID | None:
|
|
"""Translate a human case_number (e.g. '1017-03-26') to a UUID."""
|
|
if not case_number or not case_number.strip():
|
|
return None
|
|
row = await db.get_case_by_number(case_number.strip())
|
|
if not row:
|
|
return None
|
|
return UUID(row["id"])
|
|
|
|
|
|
async def missing_precedent_create(
|
|
citation: str,
|
|
case_number: str = "",
|
|
cited_in_document_id: str = "",
|
|
cited_by_party: str = "unknown",
|
|
cited_by_party_name: str = "",
|
|
legal_topic: str = "",
|
|
legal_issue: str = "",
|
|
claim_quote: str = "",
|
|
case_name: str = "",
|
|
notes: str = "",
|
|
) -> str:
|
|
"""תיעוד פסיקה שצוטטה אך אינה בקורפוס. הסוכן יוצר רשומה כשהוא מזהה ציטוט
|
|
שלא ניתן לאמת מול הקורפוס; היו"ר יסגור אותה לאחר העלאת המסמך.
|
|
|
|
Args:
|
|
citation: מראה המקום המלא (חובה).
|
|
case_number: מספר תיק הערר שבו צוטטה הפסיקה (לדוגמה '1017-03-26').
|
|
cited_in_document_id: UUID של המסמך שבו הציטוט מופיע (אופציונלי).
|
|
cited_by_party: appellant / respondent / committee / permit_applicant / unknown.
|
|
cited_by_party_name: שם הצד (כדי שיהיה ברור מי ציטט).
|
|
legal_topic: נושא משפטי קצר (לדוגמה "זכות עמידה").
|
|
legal_issue: שאלה משפטית מפורטת.
|
|
claim_quote: הציטוט בכתב הטענות.
|
|
case_name: שם קצר של פסק הדין החסר.
|
|
notes: הערות חופשיות.
|
|
|
|
Returns: JSON של הרשומה שנוצרה (כולל id) או error.
|
|
"""
|
|
if not citation.strip():
|
|
return _err("citation חובה")
|
|
|
|
case_id = None
|
|
if case_number:
|
|
case_id = await _resolve_case_id(case_number)
|
|
if case_id is None:
|
|
return _err(f"תיק לא נמצא: {case_number}")
|
|
|
|
doc_uuid: UUID | None = None
|
|
if cited_in_document_id.strip():
|
|
try:
|
|
doc_uuid = UUID(cited_in_document_id.strip())
|
|
except ValueError:
|
|
return _err("cited_in_document_id לא תקין")
|
|
|
|
party = cited_by_party.strip() or "unknown"
|
|
if party not in db.ALLOWED_MP_PARTIES:
|
|
return _err(
|
|
f"cited_by_party לא תקין. ערכים תקפים: "
|
|
f"{', '.join(sorted(db.ALLOWED_MP_PARTIES))}"
|
|
)
|
|
|
|
# Deduplication: if a row already exists for the same citation in
|
|
# the same case, return that one rather than creating a duplicate.
|
|
existing = await db.find_missing_precedent_by_citation(
|
|
citation=citation.strip(),
|
|
case_id=case_id,
|
|
)
|
|
if existing:
|
|
return _ok({**existing, "_duplicate": True})
|
|
|
|
try:
|
|
row = await db.create_missing_precedent(
|
|
citation=citation.strip(),
|
|
case_name=case_name.strip() or None,
|
|
cited_in_case_id=case_id,
|
|
cited_in_document_id=doc_uuid,
|
|
cited_by_party=party,
|
|
cited_by_party_name=cited_by_party_name.strip() or None,
|
|
legal_topic=legal_topic.strip() or None,
|
|
legal_issue=legal_issue.strip() or None,
|
|
claim_quote=claim_quote.strip() or None,
|
|
notes=notes.strip() or None,
|
|
)
|
|
except Exception as e:
|
|
return _err(str(e))
|
|
return _ok(row)
|
|
|
|
|
|
async def missing_precedent_list(
|
|
case_number: str = "",
|
|
status: str = "open",
|
|
legal_topic: str = "",
|
|
limit: int = 50,
|
|
) -> str:
|
|
"""רשימת פסיקות חסרות. ברירת מחדל = פתוחות בלבד.
|
|
|
|
Args:
|
|
case_number: סינון לפי תיק הערר שבו צוטטו.
|
|
status: open / uploaded / closed / irrelevant (ריק = הכל).
|
|
legal_topic: סינון לפי נושא משפטי (substring).
|
|
limit: מספר תוצאות מקסימלי.
|
|
|
|
Returns: JSON עם רשימת רשומות + linked_case_law_number אם נסגרו.
|
|
"""
|
|
case_id = None
|
|
if case_number:
|
|
case_id = await _resolve_case_id(case_number)
|
|
if case_id is None:
|
|
return _err(f"תיק לא נמצא: {case_number}")
|
|
|
|
s = status.strip() or None
|
|
if s and s not in db.ALLOWED_MP_STATUS:
|
|
return _err(
|
|
f"status לא תקין. ערכים תקפים: "
|
|
f"{', '.join(sorted(db.ALLOWED_MP_STATUS))}"
|
|
)
|
|
try:
|
|
rows = await db.list_missing_precedents(
|
|
status=s,
|
|
case_id=case_id,
|
|
legal_topic=legal_topic.strip() or None,
|
|
limit=max(1, min(int(limit), 500)),
|
|
)
|
|
except Exception as e:
|
|
return _err(str(e))
|
|
return _ok({"items": rows, "count": len(rows)})
|
|
|
|
|
|
async def missing_precedent_close(
|
|
id: str,
|
|
linked_case_law_id: str = "",
|
|
notes: str = "",
|
|
status: str = "closed",
|
|
) -> str:
|
|
"""סגירת רשומת פסיקה חסרה. ברירת מחדל = 'closed' + קישור ל-case_law.
|
|
|
|
Args:
|
|
id: UUID של הרשומה.
|
|
linked_case_law_id: UUID של הפסיקה שהועלתה ב-precedent_library / internal_decisions.
|
|
notes: הערות סגירה (לדוגמה "אינו רלוונטי" ל-status='irrelevant').
|
|
status: closed / uploaded / irrelevant.
|
|
|
|
Returns: JSON של הרשומה המעודכנת.
|
|
"""
|
|
try:
|
|
mp_id = UUID(id.strip())
|
|
except ValueError:
|
|
return _err("id לא תקין")
|
|
|
|
cl_uuid: UUID | None = None
|
|
if linked_case_law_id.strip():
|
|
try:
|
|
cl_uuid = UUID(linked_case_law_id.strip())
|
|
except ValueError:
|
|
return _err("linked_case_law_id לא תקין")
|
|
|
|
status_clean = status.strip() or "closed"
|
|
if status_clean not in db.ALLOWED_MP_STATUS:
|
|
return _err(
|
|
f"status לא תקין. ערכים תקפים: "
|
|
f"{', '.join(sorted(db.ALLOWED_MP_STATUS))}"
|
|
)
|
|
|
|
try:
|
|
row = await db.close_missing_precedent(
|
|
mp_id=mp_id,
|
|
linked_case_law_id=cl_uuid,
|
|
notes=notes.strip() or None,
|
|
status=status_clean,
|
|
)
|
|
except Exception as e:
|
|
return _err(str(e))
|
|
if row is None:
|
|
return _err("רשומה לא נמצאה")
|
|
return _ok(row)
|