Add case_precedents: attached legal support for the compose phase
New self-contained table + MCP tools + FastAPI endpoints for letting
the chair attach external case-law quotes (quote + citation מראה מקום,
optional chair note, optional archived PDF) to either a specific
threshold_claim / issue or the case as a whole.
Data model
- case_precedents (SCHEMA_V5_SQL) — case_id, section_id NULL/
"threshold_N"/"issue_N", quote, citation (free-text), chair_note,
pdf_document_id FK to documents, denormalized practice_area for
cross-case library filtering.
- Deliberately NOT linked to the existing case_law table — that one
has UNIQUE(case_number) which would force parsing the free-text
citation into a structured key. A backfill pass into case_law is
a later follow-up once the UI stabilizes.
- db.py gains 4 helpers: create_case_precedent, list_case_precedents,
delete_case_precedent, search_precedent_library. The last uses
DISTINCT ON (citation) for the cross-case typeahead so each
precedent appears once even if reused across many cases.
MCP tools (legal_mcp/tools/precedents.py)
- precedent_attach, precedent_list, precedent_remove,
precedent_search_library — registered in server.py.
FastAPI (web/app.py)
- POST /api/cases/{n}/precedents — create, with PrecedentCreateRequest
- POST /api/cases/{n}/precedents/upload-pdf — one-shot PDF upload to
a dedicated documents/precedents/ subdirectory, creates a
documents row with doc_type="precedent_archive" and no text
extraction (archive only)
- GET /api/cases/{n}/precedents — list
- DELETE /api/precedents/{id} — uses path param since precedent_id
is a UUID (slash-safe, unlike case numbers)
- GET /api/precedents/search?q=...&practice_area=... — library
typeahead
Block-writer integration into _build_precedents_context is a deferred
follow-up — Phase 1 surfaces the feature in the compose UI only.
Plan: ~/.claude/plans/woolly-cooking-graham.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
mcp-server/src/legal_mcp/tools/precedents.py
Normal file
95
mcp-server/src/legal_mcp/tools/precedents.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""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
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import db
|
||||
|
||||
|
||||
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 json.dumps({"error": f"תיק {case_number} לא נמצא."}, ensure_ascii=False)
|
||||
|
||||
pdf_uuid: UUID | None = None
|
||||
if pdf_document_id:
|
||||
try:
|
||||
pdf_uuid = UUID(pdf_document_id)
|
||||
except ValueError:
|
||||
return json.dumps({"error": "pdf_document_id לא תקין"}, ensure_ascii=False)
|
||||
|
||||
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 json.dumps(row, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def precedent_list(case_number: str) -> str:
|
||||
"""רשימת כל הפסיקות שצורפו לתיק, ממוינות לפי סעיף ואז לפי זמן יצירה."""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return json.dumps({"error": f"תיק {case_number} לא נמצא."}, ensure_ascii=False)
|
||||
|
||||
rows = await db.list_case_precedents(UUID(case["id"]))
|
||||
return json.dumps(rows, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def precedent_remove(precedent_id: str) -> str:
|
||||
"""הסרת פסיקה מצורפת. קובץ ה-PDF (אם צורף) נשאר ב-documents לצורך audit."""
|
||||
try:
|
||||
pid = UUID(precedent_id)
|
||||
except ValueError:
|
||||
return json.dumps({"error": "precedent_id לא תקין"}, ensure_ascii=False)
|
||||
|
||||
ok = await db.delete_case_precedent(pid)
|
||||
return json.dumps(
|
||||
{"deleted": ok, "precedent_id": precedent_id}, ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
async def precedent_search_library(
|
||||
query: str, practice_area: str = "", limit: int = 10,
|
||||
) -> str:
|
||||
"""חיפוש בספרייה הרוחבית — כל הפסיקות שצורפו אי-פעם בכל התיקים.
|
||||
|
||||
Args:
|
||||
query: מחרוזת חיפוש (מתחרה מול citation ומול quote)
|
||||
practice_area: אופציונלי — סינון לתחום משפטי מסוים
|
||||
limit: מספר תוצאות מקסימלי
|
||||
"""
|
||||
if not query or len(query.strip()) < 2:
|
||||
return json.dumps([], ensure_ascii=False)
|
||||
|
||||
rows = await db.search_precedent_library(query.strip(), practice_area, limit)
|
||||
return json.dumps(rows, ensure_ascii=False, indent=2)
|
||||
Reference in New Issue
Block a user