All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m28s
Three improvements to the precedent library based on usage feedback:
1. Auto-fill metadata at upload time. New service
precedent_metadata_extractor reads the ruling's full_text and
suggests case_name (short), summary, headnote, key_quote,
subject_tags, appeal_subtype. The merge policy fills only empty
fields, preserving everything the chair typed in the upload form.
Wired into the ingest pipeline; also exposed as a re-run endpoint
POST /api/precedent-library/{id}/extract-metadata for existing
records.
2. Edit sheet in the UI. Pencil icon on each library row opens a
pre-populated form covering every field. A Sparkles button on the
sheet runs the metadata extractor on demand and refreshes the
form. The case_number is read-only because halachot are FK'd to
it; renaming requires delete + re-upload.
3. Halacha extractor branches on is_binding. Sources marked binding
(Supreme/Administrative) keep the strict halacha prompt. Non-binding
sources (other appeals committees, district courts on planning
matters) get a different prompt that extracts applications,
interpretive principles, and persuasive conclusions — labeled with
new rule_types 'application' and 'persuasive'. The fallback also
widens chunk selection: if the chunker labeled nothing as
legal_analysis/ruling/conclusion, we now run on all chunks rather
than returning zero halachot for a usable ruling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
248 lines
8.8 KiB
Python
248 lines
8.8 KiB
Python
"""MCP tools for the External Precedent Library.
|
||
|
||
This is distinct from:
|
||
|
||
- ``precedents`` (case_precedents table) — chair-attached quotes scoped to
|
||
a specific case section. Use ``precedent_search_library`` for that.
|
||
- ``style_corpus`` (Daphna's prior decisions) — searched via
|
||
``search_decisions`` for style/voice.
|
||
|
||
The precedent library is the **authoritative law** corpus: external court
|
||
rulings and other appeals committees' decisions, with halachot extracted
|
||
and reviewed by the chair.
|
||
|
||
All halachot enter as ``pending_review`` and are invisible to search until
|
||
the chair approves them — per project review policy.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from uuid import UUID
|
||
|
||
from legal_mcp.services import db, precedent_library
|
||
|
||
|
||
def _ok(payload) -> str:
|
||
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||
|
||
|
||
def _err(msg: str) -> str:
|
||
return json.dumps({"error": msg}, ensure_ascii=False)
|
||
|
||
|
||
async def precedent_library_upload(
|
||
file_path: str,
|
||
citation: str,
|
||
case_name: str = "",
|
||
court: str = "",
|
||
decision_date: str = "",
|
||
source_type: str = "",
|
||
precedent_level: str = "",
|
||
practice_area: str = "",
|
||
appeal_subtype: str = "",
|
||
subject_tags: list[str] | None = None,
|
||
is_binding: bool = True,
|
||
headnote: str = "",
|
||
summary: str = "",
|
||
) -> str:
|
||
"""העלאת פסיקה חיצונית לקורפוס הסמכותי + חילוץ הלכות אוטומטי.
|
||
|
||
Args:
|
||
file_path: נתיב מלא לקובץ PDF/DOCX/RTF/TXT/MD.
|
||
citation: מראה המקום ("עע\\"מ 3975/22 ב. קרן-נכסים נ' ועדה מקומית").
|
||
case_name: שם קצר.
|
||
court: ערכאה (עליון / מנהלי / ועדת ערר ארצית / ועדת ערר מחוזית).
|
||
decision_date: ISO date (YYYY-MM-DD), אופציונלי.
|
||
source_type: court_ruling / appeals_committee.
|
||
precedent_level: עליון / מנהלי / ועדת_ערר_ארצית / ועדת_ערר_מחוזית.
|
||
practice_area: rishuy_uvniya / betterment_levy / compensation_197.
|
||
subject_tags: תגיות נושא (חניה, קווי_בניין, וכד').
|
||
|
||
Returns: JSON עם case_law_id, מספר chunks, מספר הלכות שנכנסו לתור אישור.
|
||
"""
|
||
if not citation.strip():
|
||
return _err("citation חובה")
|
||
try:
|
||
result = await precedent_library.ingest_precedent(
|
||
file_path=file_path,
|
||
citation=citation,
|
||
case_name=case_name,
|
||
court=court,
|
||
decision_date=decision_date or None,
|
||
source_type=source_type,
|
||
precedent_level=precedent_level,
|
||
practice_area=practice_area,
|
||
appeal_subtype=appeal_subtype,
|
||
subject_tags=subject_tags or [],
|
||
is_binding=is_binding,
|
||
headnote=headnote,
|
||
summary=summary,
|
||
)
|
||
except Exception as e:
|
||
return _err(str(e))
|
||
return _ok(result)
|
||
|
||
|
||
async def precedent_library_list(
|
||
practice_area: str = "",
|
||
court: str = "",
|
||
precedent_level: str = "",
|
||
source_type: str = "",
|
||
search: str = "",
|
||
limit: int = 100,
|
||
) -> str:
|
||
"""רשימה של פסיקה בקורפוס הסמכותי, עם פילטרים."""
|
||
rows = await precedent_library.list_precedents(
|
||
practice_area=practice_area,
|
||
court=court,
|
||
precedent_level=precedent_level,
|
||
source_type=source_type,
|
||
search=search,
|
||
limit=limit,
|
||
)
|
||
return _ok(rows)
|
||
|
||
|
||
async def precedent_library_get(case_law_id: str) -> str:
|
||
"""פסיקה ספציפית עם כל ההלכות שלה (כולל ממתינות לאישור)."""
|
||
try:
|
||
cid = UUID(case_law_id)
|
||
except ValueError:
|
||
return _err("case_law_id לא תקין")
|
||
record = await precedent_library.get_precedent(cid)
|
||
if not record:
|
||
return _err("פסיקה לא נמצאה")
|
||
return _ok(record)
|
||
|
||
|
||
async def precedent_library_delete(case_law_id: str) -> str:
|
||
"""מחיקת פסיקה מהקורפוס. cascade: chunks + halachot."""
|
||
try:
|
||
cid = UUID(case_law_id)
|
||
except ValueError:
|
||
return _err("case_law_id לא תקין")
|
||
ok = await precedent_library.delete_precedent(cid)
|
||
return _ok({"deleted": ok, "case_law_id": case_law_id})
|
||
|
||
|
||
async def precedent_extract_halachot(case_law_id: str) -> str:
|
||
"""הרצה מחדש של חילוץ ההלכות לפסיקה קיימת. הלכות קודמות נמחקות."""
|
||
try:
|
||
cid = UUID(case_law_id)
|
||
except ValueError:
|
||
return _err("case_law_id לא תקין")
|
||
try:
|
||
result = await precedent_library.reextract_halachot(cid)
|
||
except Exception as e:
|
||
return _err(str(e))
|
||
return _ok(result)
|
||
|
||
|
||
async def precedent_extract_metadata(case_law_id: str) -> str:
|
||
"""חילוץ מטא-דאטה (case_name קצר, summary, headnote, key_quote, subject_tags, appeal_subtype) מהטקסט. ממלא רק שדות ריקים — לא דורס מה שכבר הוזן."""
|
||
try:
|
||
cid = UUID(case_law_id)
|
||
except ValueError:
|
||
return _err("case_law_id לא תקין")
|
||
try:
|
||
result = await precedent_library.reextract_metadata(cid)
|
||
except Exception as e:
|
||
return _err(str(e))
|
||
return _ok(result)
|
||
|
||
|
||
async def search_precedent_library(
|
||
query: str,
|
||
practice_area: str = "",
|
||
court: str = "",
|
||
precedent_level: str = "",
|
||
appeal_subtype: str = "",
|
||
is_binding: bool | None = None,
|
||
subject_tag: str = "",
|
||
limit: int = 10,
|
||
include_halachot: bool = True,
|
||
) -> str:
|
||
"""חיפוש סמנטי בקורפוס הפסיקה הסמכותית.
|
||
|
||
מחזיר תוצאות מעורבות: הלכות (rule-level, מאושרות בלבד) + קטעי טקסט
|
||
(passage-level). הלכות מקבלות boost קל בדירוג כי הן מזוקקות מראש.
|
||
|
||
Args:
|
||
query: שאילתת חיפוש בעברית.
|
||
practice_area: rishuy_uvniya / betterment_levy / compensation_197.
|
||
court: סינון לפי ערכאה (substring).
|
||
precedent_level: עליון / מנהלי / ועדת_ערר_ארצית / ועדת_ערר_מחוזית.
|
||
appeal_subtype: סינון לתת-סוג.
|
||
is_binding: True/False (None = ללא סינון).
|
||
subject_tag: סינון לפי תגית נושא (לדוגמה "מועד_קביעת_שומה").
|
||
limit: מספר תוצאות מקסימלי.
|
||
include_halachot: האם לכלול הלכות (ברירת מחדל: כן).
|
||
|
||
Returns: רשימה מדורגת. כל פריט הוא {"type": "halacha"|"passage", "score", ...}.
|
||
"""
|
||
if not query or len(query.strip()) < 2:
|
||
return json.dumps([], ensure_ascii=False)
|
||
results = await precedent_library.search_library(
|
||
query=query.strip(),
|
||
practice_area=practice_area,
|
||
court=court,
|
||
precedent_level=precedent_level,
|
||
appeal_subtype=appeal_subtype,
|
||
is_binding=is_binding,
|
||
subject_tag=subject_tag,
|
||
limit=limit,
|
||
include_halachot=include_halachot,
|
||
)
|
||
return _ok(results)
|
||
|
||
|
||
async def halacha_review(
|
||
halacha_id: str,
|
||
status: str,
|
||
reviewer: str = "דפנה",
|
||
rule_statement: str = "",
|
||
reasoning_summary: str = "",
|
||
subject_tags: list[str] | None = None,
|
||
practice_areas: list[str] | None = None,
|
||
) -> str:
|
||
"""אישור / דחייה / עריכה של הלכה שחולצה אוטומטית.
|
||
|
||
Args:
|
||
halacha_id: מזהה ההלכה.
|
||
status: pending_review / approved / rejected / published.
|
||
reviewer: שם המאשר (ברירת מחדל: דפנה).
|
||
rule_statement: עריכת ניסוח הכלל (ריק = ללא שינוי).
|
||
reasoning_summary: עריכת תמצית ההיגיון (ריק = ללא שינוי).
|
||
subject_tags: עריכת תגיות (None = ללא שינוי).
|
||
practice_areas: עריכת תחומים (None = ללא שינוי).
|
||
"""
|
||
if status not in {"pending_review", "approved", "rejected", "published"}:
|
||
return _err(
|
||
"status לא חוקי. ערכים תקינים: "
|
||
"pending_review / approved / rejected / published"
|
||
)
|
||
try:
|
||
hid = UUID(halacha_id)
|
||
except ValueError:
|
||
return _err("halacha_id לא תקין")
|
||
|
||
row = await db.update_halacha(
|
||
halacha_id=hid,
|
||
review_status=status,
|
||
reviewer=reviewer,
|
||
rule_statement=rule_statement or None,
|
||
reasoning_summary=reasoning_summary or None,
|
||
subject_tags=subject_tags,
|
||
practice_areas=practice_areas,
|
||
)
|
||
if row is None:
|
||
return _err("הלכה לא נמצאה")
|
||
return _ok(row)
|
||
|
||
|
||
async def halachot_pending(limit: int = 100) -> str:
|
||
"""תור ההלכות הממתינות לאישור (review_status='pending_review')."""
|
||
rows = await db.list_halachot(review_status="pending_review", limit=limit)
|
||
return _ok(rows)
|