INV-TOOL2: `precedent_search_library` (שמחפש ציטוטים מצורפים-לתיק) היה הפוך וכמעט-זהה ל-`search_precedent_library` (ספריית-הפסיקה הסמכותית, מקור CREAC), מה שסיכן ציטוט מהמקור הלא-נכון בהחלטה. שונה ל-`search_case_precedents` (שם ברור: case-attached). השם הישן נשמר כ-@mcp.tool() alias deprecated המנתב לחדש → אפס שבירה לסוכנים חיים. docstrings של שני כלי-הפסיקה הובהרו (case-attached מול authoritative). עודכנו: web/app.py (typeahead), legal-researcher/legal-writer docs, precedent_library docstring. 5 כלי-החיפוש הנותרים (search_decisions/case_documents/find_similar/internal/ precedent_library) מחפשים קורפוסים מובחנים בשמות סבירים — לא בוצע rename המוני (churn גבוה, ערך נמוך מול הסיכון). בדיקות: 182/182 עוברים. אחרי deploy — סנכרון cross-company של doc-הסוכן. Invariants: מקדם INV-TOOL2 + G2. מתועד ב-X9 + gap-audit פרוסה 8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
"""MCP tools for attached legal precedents (user-supplied case-law quotes).
|
|
|
|
These complement the existing `case_law` table (which is populated from
|
|
structured sources and is what the block-writer RAG searches) by storing
|
|
free-text citations the chair attaches during the compose phase.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from legal_mcp.services import db
|
|
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
|
|
|
|
|
|
async def precedent_attach(
|
|
case_number: str,
|
|
quote: str,
|
|
citation: str,
|
|
section_id: str = "",
|
|
chair_note: str = "",
|
|
pdf_document_id: str = "",
|
|
) -> str:
|
|
"""צירוף פסיקה תומכת לתיק ערר.
|
|
|
|
Args:
|
|
case_number: מספר תיק הערר
|
|
quote: הציטוט המדויק שיוכנס להחלטה
|
|
citation: מראה המקום (ערר 1126-08-25 ... נ' ... (נבו 9.3.2026))
|
|
section_id: מזהה הטענה/סוגיה (threshold_1, issue_3); ריק = כללי לתיק
|
|
chair_note: הערה אופציונלית — למה הציטוט תומך בעמדה
|
|
pdf_document_id: מזהה קובץ PDF מצורף (אופציונלי)
|
|
"""
|
|
case = await db.get_case_by_number(case_number)
|
|
if not case:
|
|
return err(f"תיק {case_number} לא נמצא.")
|
|
|
|
pdf_uuid: UUID | None = None
|
|
if pdf_document_id:
|
|
try:
|
|
pdf_uuid = UUID(pdf_document_id)
|
|
except ValueError:
|
|
return err("pdf_document_id לא תקין")
|
|
|
|
# INV-TOOL3 / GAP-52: idempotent on (case_id, section_id, citation, quote).
|
|
# Re-attaching the same quote to the same section returns the existing row.
|
|
for _p in await db.list_case_precedents(UUID(case["id"])):
|
|
if (_p.get("citation") == citation and _p.get("quote") == quote
|
|
and (_p.get("section_id") or None) == (section_id or None)):
|
|
_p["idempotent_existing"] = True
|
|
return ok(_p)
|
|
|
|
row = await db.create_case_precedent(
|
|
case_id=UUID(case["id"]),
|
|
quote=quote,
|
|
citation=citation,
|
|
section_id=section_id or None,
|
|
chair_note=chair_note,
|
|
pdf_document_id=pdf_uuid,
|
|
practice_area=case.get("practice_area"),
|
|
)
|
|
return ok(row)
|
|
|
|
|
|
async def precedent_list(case_number: str) -> str:
|
|
"""רשימת כל הפסיקות שצורפו לתיק, ממוינות לפי סעיף ואז לפי זמן יצירה."""
|
|
case = await db.get_case_by_number(case_number)
|
|
if not case:
|
|
return err(f"תיק {case_number} לא נמצא.")
|
|
|
|
rows = await db.list_case_precedents(UUID(case["id"]))
|
|
return ok(rows)
|
|
|
|
|
|
async def precedent_remove(precedent_id: str) -> str:
|
|
"""הסרת פסיקה מצורפת. קובץ ה-PDF (אם צורף) נשאר ב-documents לצורך audit."""
|
|
try:
|
|
pid = UUID(precedent_id)
|
|
except ValueError:
|
|
return err("precedent_id לא תקין")
|
|
|
|
deleted = await db.delete_case_precedent(pid)
|
|
return ok({"deleted": deleted, "precedent_id": precedent_id})
|
|
|
|
|
|
async def search_case_precedents(
|
|
query: str, practice_area: str = "", limit: int = 10,
|
|
) -> str:
|
|
"""חיפוש רוחבי בציטוטי-הפסיקה שצורפו ידנית לתיקים (case_precedents) — קורפוס
|
|
"case-attached". GAP-49 (INV-TOOL2): שם קודם `precedent_search_library` (מטעה).
|
|
זו **אינה** ספריית-הפסיקה הסמכותית — לזו השתמש ב-`search_precedent_library`.
|
|
|
|
Args:
|
|
query: מחרוזת חיפוש (מתחרה מול citation ומול quote)
|
|
practice_area: אופציונלי — סינון לתחום משפטי מסוים
|
|
limit: מספר תוצאות מקסימלי
|
|
"""
|
|
if not query or len(query.strip()) < 2:
|
|
return empty("שאילתה קצרה מדי (פחות מ-2 תווים).")
|
|
|
|
rows = await db.search_precedent_library(query.strip(), practice_area, limit)
|
|
return ok(rows)
|