הכרעת-יו"ר: קנוני = 3 תוצאות אמיתיות (rejection/partial_acceptance/full_acceptance); betterment_levy יוצא מהיותו "תוצאה" ועובר ל-override לפי practice_area. + עקרון "אנגלית-ב-DB, עברית-ב-UI": מפת-תוויות SSoT אחת. lessons.py: - VALID_OUTCOMES = 3 (הוסר betterment_levy). - OUTCOME_LABELS_HE (SSoT לתצוגה) + LEGACY_OUTCOME_MAP + canonical_outcome(). - PRACTICE_AREA_OVERRIDES["betterment_levy"] מרכז את כל ה-guidance שהיה מפתוח כ-outcome (golden_ratios/opening/summary/discussion/template). - get_lessons_for_outcome(outcome, practice_area) + format_ratios_comment(..., practice_area) מחילים override + מנרמלים legacy. block_writer.py: STRUCTURE_GUIDANCE קנוני + תווית מ-OUTCOME_LABELS_HE + override betterment. workflow.set_outcome: קנוני 3 + מיפוי-legacy סלחני; תווית מ-SSoT. drafting.py: טבלת יחסי-זהב + get_decision_template מודעי-practice_area (override). web-ui case.ts: הסרת betterment_levy מ-expectedOutcomes (הוא practice_area). server.py: docstrings קנוניים. מיגרציה: migrate_gap51_outcomes.py — 9 שורות נורמלו (rejected→rejection וכו'), גיבוי ב-data/audit/. הקוד canonicalize בקריאה ⇒ backward-compatible גם בלי מיגרציה. אומת: py_compile (5 קבצים) + בדיקות-יחידה offline (override/legacy/labels) + אימות-DB. עודכנו X9 §3 + gap-audit (GAP-51 ✅). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
981 lines
36 KiB
Python
981 lines
36 KiB
Python
"""Ezer Mishpati - MCP Server entry point.
|
||
|
||
Run with: python -m legal_mcp.server
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import sys
|
||
from collections.abc import AsyncIterator
|
||
from contextlib import asynccontextmanager
|
||
|
||
from mcp.server.fastmcp import FastMCP
|
||
|
||
# Configure logging to stderr (stdout is reserved for JSON-RPC)
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||
stream=sys.stderr,
|
||
)
|
||
logger = logging.getLogger("legal_mcp")
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(server: FastMCP) -> AsyncIterator[None]:
|
||
"""Server startup is now non-blocking.
|
||
|
||
Schema init was moved out of the lifespan to fix a race where Claude Code
|
||
would call a tool before `tools/list` had been answered — manifesting as
|
||
"No such tool available". Lifespan now returns immediately so the MCP
|
||
handshake completes in milliseconds; the schema is initialized lazily on
|
||
the first DB access via services/db.get_pool().
|
||
"""
|
||
from legal_mcp.services.db import close_pool
|
||
|
||
logger.info("Ezer Mishpati MCP server ready (schema init deferred)")
|
||
try:
|
||
yield
|
||
finally:
|
||
await close_pool()
|
||
logger.info("Ezer Mishpati MCP server stopped")
|
||
|
||
|
||
# Create MCP server
|
||
mcp = FastMCP(
|
||
"Ezer Mishpati - עוזר משפטי",
|
||
instructions="מערכת AI לסיוע בניסוח החלטות משפטיות בסגנון דפנה תמיר",
|
||
lifespan=lifespan,
|
||
)
|
||
|
||
# ── Import and register tools ───────────────────────────────────────
|
||
|
||
from legal_mcp.tools import ( # noqa: E402
|
||
cases, documents, search, drafting, workflow, precedents,
|
||
precedent_library as plib,
|
||
internal_decisions as int_tools,
|
||
legal_arguments as la_tools,
|
||
missing_precedents as mp_tools,
|
||
citations as cit_tools,
|
||
training_enrichment as train_tools,
|
||
)
|
||
|
||
|
||
# Case management
|
||
@mcp.tool()
|
||
async def case_create(
|
||
case_number: str,
|
||
title: str,
|
||
appellants: list[str] | None = None,
|
||
respondents: list[str] | None = None,
|
||
subject: str = "",
|
||
property_address: str = "",
|
||
permit_number: str = "",
|
||
committee_type: str = "ועדה מקומית",
|
||
hearing_date: str = "",
|
||
notes: str = "",
|
||
expected_outcome: str = "",
|
||
) -> str:
|
||
"""יצירת תיק ערר חדש. expected_outcome: rejection/partial_acceptance/full_acceptance/betterment_levy."""
|
||
return await cases.case_create(
|
||
case_number, title, appellants, respondents,
|
||
subject, property_address, permit_number, committee_type,
|
||
hearing_date, notes, expected_outcome,
|
||
)
|
||
|
||
|
||
# INV-TOOL5 / GAP-53: hard cap on list/search result sizes (OWASP API4:2023 —
|
||
# Unrestricted Resource Consumption). Non-positive is treated as "max", not "all".
|
||
_MAX_LIMIT = 200
|
||
|
||
|
||
def _clamp_limit(limit: int, hard_max: int = _MAX_LIMIT) -> int:
|
||
"""Clamp a caller-supplied result limit to [1, hard_max]."""
|
||
try:
|
||
n = int(limit)
|
||
except (TypeError, ValueError):
|
||
return hard_max
|
||
return hard_max if n <= 0 else min(n, hard_max)
|
||
|
||
|
||
@mcp.tool()
|
||
async def case_list(status: str = "", limit: int = 50) -> str:
|
||
"""רשימת תיקי ערר. סינון אופציונלי לפי סטטוס (new/in_progress/drafted/reviewed/final)."""
|
||
return await cases.case_list(status, _clamp_limit(limit))
|
||
|
||
|
||
@mcp.tool()
|
||
async def case_get(case_number: str) -> str:
|
||
"""פרטי תיק מלאים כולל רשימת מסמכים."""
|
||
return await cases.case_get(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def case_update(
|
||
case_number: str,
|
||
status: str = "",
|
||
title: str = "",
|
||
subject: str = "",
|
||
notes: str = "",
|
||
hearing_date: str = "",
|
||
decision_date: str = "",
|
||
tags: list[str] | None = None,
|
||
expected_outcome: str = "",
|
||
) -> str:
|
||
"""עדכון פרטי תיק. expected_outcome: rejection/partial_acceptance/full_acceptance (betterment_levy הוא practice_area, לא תוצאה)."""
|
||
return await cases.case_update(
|
||
case_number, status, title, subject, notes,
|
||
hearing_date, decision_date, tags, expected_outcome,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def case_delete(case_number: str, remove_files: bool = False) -> str:
|
||
"""מחיקת תיק ערר. קבצים בדיסק נשארים אלא אם remove_files=true."""
|
||
return await cases.case_delete(case_number, remove_files)
|
||
|
||
|
||
@mcp.tool()
|
||
async def case_get_final_text(case_number: str, max_chars: int = 0) -> str:
|
||
"""קליטת טקסט ההחלטה הסופית (`סופי-{case}.docx` בתיקיית exports).
|
||
max_chars: 0=הכל, אחרת חיתוך לאורך הנתון. שימושי ל-Hermes Knowledge Curator."""
|
||
return await cases.case_get_final_text(case_number, max_chars)
|
||
|
||
|
||
# Precedent attachments (user-supplied legal support for the compose phase)
|
||
@mcp.tool()
|
||
async def precedent_attach(
|
||
case_number: str,
|
||
quote: str,
|
||
citation: str,
|
||
section_id: str = "",
|
||
chair_note: str = "",
|
||
pdf_document_id: str = "",
|
||
) -> str:
|
||
"""צירוף פסיקה תומכת לתיק. section_id ריק = כללי לתיק; אחרת threshold_1/issue_3."""
|
||
return await precedents.precedent_attach(
|
||
case_number, quote, citation, section_id, chair_note, pdf_document_id,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_list(case_number: str) -> str:
|
||
"""רשימת כל הפסיקות שצורפו לתיק."""
|
||
return await precedents.precedent_list(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_remove(precedent_id: str) -> str:
|
||
"""הסרת פסיקה מצורפת."""
|
||
return await precedents.precedent_remove(precedent_id)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_search_library(
|
||
query: str, practice_area: str = "", limit: int = 10,
|
||
) -> str:
|
||
"""חיפוש בציטוטים שדפנה צירפה ידנית לתיקים בעבר (case_precedents).
|
||
שונה מ-search_precedent_library שמחפש בקורפוס הפסיקה הסמכותית."""
|
||
return await precedents.precedent_search_library(query, practice_area, _clamp_limit(limit))
|
||
|
||
|
||
# ── External Precedent Library — authoritative case-law corpus ─────
|
||
# Distinct from precedent_search_library above (chair-attached quotes)
|
||
# and from search_decisions (Daphna's style corpus).
|
||
|
||
|
||
@mcp.tool()
|
||
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:
|
||
"""העלאת פסיקה חיצונית (פס"ד / החלטה של ועדה אחרת) לקורפוס הסמכותי. מחלץ הלכות אוטומטית — כולן ממתינות לאישור היו"ר. practice_area: rishuy_uvniya / betterment_levy / compensation_197."""
|
||
return await plib.precedent_library_upload(
|
||
file_path, citation, case_name, court, decision_date,
|
||
source_type, precedent_level, practice_area, appeal_subtype,
|
||
subject_tags, is_binding, headnote, summary,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_library_list(
|
||
practice_area: str = "",
|
||
court: str = "",
|
||
precedent_level: str = "",
|
||
source_type: str = "",
|
||
search: str = "",
|
||
source_kind: str = "external_upload",
|
||
limit: int = 100,
|
||
) -> str:
|
||
"""רשימת הפסיקה בקורפוס, עם פילטרים.
|
||
|
||
source_kind: 'external_upload' (ברירת מחדל — פס"ד בתי משפט) /
|
||
'internal_committee' (החלטות ועדות ערר ערר/בל"מ שהועלו) /
|
||
'all_committees' (שתיהן — internal + appeals_committee).
|
||
החלטות ערר/בל"מ שמעלים נשמרות כ-internal_committee — כדי לראותן
|
||
ברשימה השתמש ב-source_kind='internal_committee' או 'all_committees'.
|
||
"""
|
||
return await plib.precedent_library_list(
|
||
practice_area, court, precedent_level, source_type, search,
|
||
source_kind, _clamp_limit(limit),
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_library_get(case_law_id: str) -> str:
|
||
"""פסיקה ספציפית בקורפוס + רשימת ההלכות שחולצו ממנה (כולל ממתינות לאישור)."""
|
||
return await plib.precedent_library_get(case_law_id)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_library_delete(case_law_id: str) -> str:
|
||
"""מחיקת פסיקה מהקורפוס (cascade: chunks + halachot)."""
|
||
return await plib.precedent_library_delete(case_law_id)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_link_cases(
|
||
case_law_id_a: str,
|
||
case_law_id_b: str,
|
||
relation_type: str = "same_case_chain",
|
||
) -> str:
|
||
"""קישור שתי פסיקות כקשורות (דו-כיווני, idempotent). relation_type: same_case_chain | overruled_by | distinguished."""
|
||
return await plib.precedent_link_cases(case_law_id_a, case_law_id_b, relation_type)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_unlink_cases(case_law_id_a: str, case_law_id_b: str) -> str:
|
||
"""הסרת קישור בין שתי פסיקות (דו-כיווני)."""
|
||
return await plib.precedent_unlink_cases(case_law_id_a, case_law_id_b)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_extract_halachot(case_law_id: str) -> str:
|
||
"""הרצה מחדש של חילוץ הלכות לפסיקה קיימת. ההלכות הקיימות נמחקות, החדשות חוזרות לסטטוס pending_review."""
|
||
return await plib.precedent_extract_halachot(case_law_id)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_extract_metadata(case_law_id: str) -> str:
|
||
"""חילוץ מטא-דאטה (case_name קצר, summary, headnote, key_quote, subject_tags, appeal_subtype, date, level, court, source_type) מהטקסט. ממלא רק שדות ריקים."""
|
||
return await plib.precedent_extract_metadata(case_law_id)
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_reindex(case_law_id: str) -> str:
|
||
"""re-chunk + re-embed פסיקה קיימת מה-full_text השמור (FU-3/GAP-09). אינו מריץ OCR/LLM — רק chunking + voyage embeddings. idempotent."""
|
||
return await plib.precedent_reindex(case_law_id)
|
||
|
||
|
||
@mcp.tool()
|
||
async def style_corpus_enrich(corpus_id: str, overwrite: bool = False) -> str:
|
||
"""חילוץ מטא-דאטה (summary, outcome, key_principles, appeal_subtype) להחלטה בקורפוס הסגנון של דפנה. ברירת מחדל: ממלא רק שדות ריקים. שלח `overwrite=true` כדי לרענן."""
|
||
return await train_tools.extract_decision_metadata(corpus_id, overwrite=overwrite)
|
||
|
||
|
||
@mcp.tool()
|
||
async def style_corpus_pending_enrichment(limit: int = 50) -> str:
|
||
"""רשימת החלטות בקורפוס הסגנון שעדיין חסרות summary/outcome/key_principles — מועמדות לחילוץ."""
|
||
return await train_tools.list_corpus_pending_enrichment(_clamp_limit(limit))
|
||
|
||
|
||
@mcp.tool()
|
||
async def extraction_status() -> str:
|
||
"""סטטוס תור-החילוץ — כמה פסיקות ממתינות לחילוץ metadata/halacha + גיל הבקשה הוותיקה. read-only (חושף את התור ש-precedent_process_pending מרוקן)."""
|
||
return await plib.extraction_status()
|
||
|
||
|
||
@mcp.tool()
|
||
async def precedent_process_pending(kind: str = "metadata", limit: int = 20) -> str:
|
||
"""ריקון תור בקשות חילוץ שנשלחו מ-UI. kind: 'metadata' או 'halacha'. מריץ extractor מקומית עם CLI על כל פריט בתור, ומנקה את הסימון אחרי הצלחה."""
|
||
return await plib.precedent_process_pending(kind, _clamp_limit(limit))
|
||
|
||
|
||
@mcp.tool()
|
||
async def search_precedent_library(
|
||
query: str,
|
||
practice_area: str = "",
|
||
court: str = "",
|
||
precedent_level: str = "",
|
||
appeal_subtype: str = "",
|
||
subject_tag: str = "",
|
||
limit: int = 10,
|
||
include_halachot: bool = True,
|
||
) -> str:
|
||
"""חיפוש סמנטי בקורפוס הפסיקה הסמכותית. מחזיר הלכות (מאושרות בלבד) + קטעי טקסט. השתמש כש-legal-writer צריך לצטט פסיקה מחייבת בבלוק י (CREAC: rule + explanation)."""
|
||
return await plib.search_precedent_library(
|
||
query, practice_area, court, precedent_level, appeal_subtype,
|
||
None, subject_tag, _clamp_limit(limit), include_halachot,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
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:
|
||
"""אישור / דחייה / עריכה של הלכה שחולצה אוטומטית. status: pending_review / approved / rejected / published."""
|
||
return await plib.halacha_review(
|
||
halacha_id, status, reviewer, rule_statement, reasoning_summary,
|
||
subject_tags, practice_areas,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def halachot_pending(limit: int = 100) -> str:
|
||
"""תור ההלכות הממתינות לאישור."""
|
||
return await plib.halachot_pending(_clamp_limit(limit))
|
||
|
||
|
||
# Documents
|
||
@mcp.tool()
|
||
async def document_upload(
|
||
case_number: str,
|
||
file_path: str,
|
||
doc_type: str = "appeal",
|
||
title: str = "",
|
||
) -> str:
|
||
"""העלאה ועיבוד מסמך לתיק ערר (PDF/DOCX/RTF/TXT). מחלץ טקסט ויוצר embeddings."""
|
||
return await documents.document_upload(case_number, file_path, doc_type, title)
|
||
|
||
|
||
@mcp.tool()
|
||
async def document_upload_training(
|
||
file_path: str,
|
||
decision_number: str = "",
|
||
decision_date: str = "",
|
||
subject_categories: list[str] | None = None,
|
||
title: str = "",
|
||
practice_area: str = "appeals_committee",
|
||
appeal_subtype: str = "",
|
||
) -> str:
|
||
"""העלאת החלטה קודמת של דפנה לקורפוס הסגנון. קטגוריות: בנייה, שימוש חורג, תכנית, היתר, הקלה, חלוקה, תמ"א 38, היטל השבחה, פיצויים 197. סוג ערר: building_permit / betterment_levy / compensation_197 (ריק = אוטומטי ממספר ההחלטה)."""
|
||
return await documents.document_upload_training(
|
||
file_path, decision_number, decision_date, subject_categories, title,
|
||
practice_area, appeal_subtype,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def document_get_text(case_number: str, doc_title: str = "") -> str:
|
||
"""קבלת טקסט מלא של מסמך מתוך תיק."""
|
||
return await documents.document_get_text(case_number, doc_title)
|
||
|
||
|
||
@mcp.tool()
|
||
async def document_list(case_number: str) -> str:
|
||
"""רשימת מסמכים בתיק."""
|
||
return await documents.document_list(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def document_update(
|
||
case_number: str,
|
||
doc_id: str,
|
||
doc_type: str = "",
|
||
appraiser_side: str = "",
|
||
) -> str:
|
||
"""עדכון תיוג מסמך — doc_type ו/או appraiser_side (committee/appellant/deciding). ריק = ללא שינוי."""
|
||
return await documents.document_update(case_number, doc_id, doc_type, appraiser_side)
|
||
|
||
|
||
# Claims extraction
|
||
@mcp.tool()
|
||
async def extract_claims(
|
||
case_number: str,
|
||
doc_title: str = "",
|
||
party_hint: str = "",
|
||
) -> str:
|
||
"""חילוץ טענות מכתב טענות בתיק. מחלץ טענות לפי צד ושומר ב-DB."""
|
||
return await documents.extract_claims(case_number, doc_title, party_hint)
|
||
|
||
|
||
@mcp.tool()
|
||
async def get_claims(
|
||
case_number: str,
|
||
party_role: str = "",
|
||
) -> str:
|
||
"""שליפת טענות שחולצו לתיק. party_role: appellant/respondent/committee/permit_applicant (ריק=הכל)."""
|
||
return await documents.get_claims(case_number, party_role)
|
||
|
||
|
||
# Legal arguments — aggregated (de-duped) propositions
|
||
@mcp.tool()
|
||
async def aggregate_claims_to_arguments(
|
||
case_number: str,
|
||
force: bool = False,
|
||
) -> str:
|
||
"""כינוס פרופוזיציות גולמיות (claims) לטיעונים משפטיים מובחנים — ~6-12 לכל צד.
|
||
|
||
משתמש ב-Claude headless לסיווג ואיגוד. force=True מוחק טיעונים קיימים לפני חישוב מחדש.
|
||
"""
|
||
return await la_tools.aggregate_claims_to_arguments(case_number, force=force)
|
||
|
||
|
||
@mcp.tool()
|
||
async def get_legal_arguments(
|
||
case_number: str,
|
||
party: str = "",
|
||
) -> str:
|
||
"""שליפת טיעונים משפטיים מאוגדים. party: appellant/respondent/committee/permit_applicant (ריק=הכל)."""
|
||
return await la_tools.get_legal_arguments(case_number, party)
|
||
|
||
|
||
# References
|
||
@mcp.tool()
|
||
async def extract_references(
|
||
case_number: str,
|
||
doc_title: str = "",
|
||
) -> str:
|
||
"""זיהוי תכניות, פסיקה וחקיקה מתוך מסמכי תיק. מזהה ומקשר להפניות קיימות ב-DB."""
|
||
return await documents.extract_references(case_number, doc_title)
|
||
|
||
|
||
# Search
|
||
@mcp.tool()
|
||
async def search_decisions(
|
||
query: str,
|
||
limit: int = 10,
|
||
section_type: str = "",
|
||
practice_area: str = "",
|
||
appeal_subtype: str = "",
|
||
case_number: str = "",
|
||
) -> str:
|
||
"""חיפוש סמנטי בהחלטות קודמות ובמסמכים — מסונן לפי תחום משפטי."""
|
||
return await search.search_decisions(
|
||
query, _clamp_limit(limit), section_type, practice_area, appeal_subtype, case_number,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def search_case_documents(
|
||
case_number: str,
|
||
query: str,
|
||
limit: int = 10,
|
||
) -> str:
|
||
"""חיפוש סמנטי בתוך מסמכי תיק ספציפי."""
|
||
return await search.search_case_documents(case_number, query, _clamp_limit(limit))
|
||
|
||
|
||
@mcp.tool()
|
||
async def find_similar_cases(
|
||
description: str,
|
||
limit: int = 5,
|
||
practice_area: str = "",
|
||
appeal_subtype: str = "",
|
||
case_number: str = "",
|
||
) -> str:
|
||
"""מציאת תיקים דומים על בסיס תיאור — מסונן לפי תחום משפטי."""
|
||
return await search.find_similar_cases(
|
||
description, _clamp_limit(limit), practice_area, appeal_subtype, case_number,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def search_internal_decisions(
|
||
query: str,
|
||
practice_area: str = "",
|
||
appeal_subtype: str = "",
|
||
district: str = "",
|
||
chair_name: str = "",
|
||
limit: int = 10,
|
||
include_halachot: bool = True,
|
||
include_cited_by: bool = False,
|
||
) -> str:
|
||
"""חיפוש בהחלטות ועדות ערר לתכנון ובנייה (כל המחוזות).
|
||
|
||
מחזיר החלטות מהקורפוס הפנימי של ועדות הערר — נפרד מפסיקת בתי המשפט.
|
||
השתמש בו במקביל ל-search_precedent_library להצגת שתי שכבות נפרדות.
|
||
|
||
Args:
|
||
query: שאילתת חיפוש בעברית
|
||
practice_area: rishuy_uvniya / betterment_levy / compensation_197
|
||
appeal_subtype: סינון לפי תת-סוג ערר
|
||
district: מחוז — ירושלים / מרכז / תל אביב / צפון / דרום / ארצי. ריק = כל המחוזות
|
||
chair_name: שם יו"ר הוועדה לסינון. ריק = כל היו"רים
|
||
limit: מספר תוצאות מקסימלי
|
||
include_halachot: האם לכלול הלכות שחולצו
|
||
include_cited_by: True = הוסף תוצאות עקיפות — לכל hit הוסף גם החלטות
|
||
שהוא מצטט (מתוך citation graph). שימושי לחיפוש "כל הקשור ל-X"
|
||
כשרוצים להרחיב מעבר לטקסט המקורי. default False.
|
||
"""
|
||
return await search.search_internal_decisions(
|
||
query, practice_area, appeal_subtype, district, chair_name, _clamp_limit(limit), include_halachot,
|
||
include_cited_by=include_cited_by,
|
||
)
|
||
|
||
|
||
# Drafting
|
||
@mcp.tool()
|
||
async def get_style_guide() -> str:
|
||
"""שליפת דפוסי הסגנון של דפנה - נוסחאות, ביטויים אופייניים ומבנה."""
|
||
return await drafting.get_style_guide()
|
||
|
||
|
||
@mcp.tool()
|
||
async def draft_section(
|
||
case_number: str,
|
||
section: str,
|
||
instructions: str = "",
|
||
) -> str:
|
||
"""הרכבת הקשר מלא לניסוח סעיף (עובדות + תקדימים + סגנון)."""
|
||
return await drafting.draft_section(case_number, section, instructions)
|
||
|
||
|
||
@mcp.tool()
|
||
async def get_chair_directions(case_number: str) -> str:
|
||
"""שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר כ-direction_doc לכותב.
|
||
קורא מ-analysis-and-research.md שמולא ע"י דפנה דרך ה-UI.
|
||
מחזיר סטטוס (missing/empty/partial/complete) + עמדות מובנות.
|
||
"""
|
||
return await drafting.get_chair_directions(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def get_decision_template(case_number: str) -> str:
|
||
"""תבנית מבנית להחלטה מלאה עם פרטי התיק."""
|
||
return await drafting.get_decision_template(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def get_block_context(
|
||
case_number: str,
|
||
block_id: str,
|
||
instructions: str = "",
|
||
) -> str:
|
||
"""קבלת הקשר מלא לכתיבת בלוק — ללא API. Claude Code כותב ושומר."""
|
||
return await drafting.get_block_context(case_number, block_id, instructions)
|
||
|
||
|
||
@mcp.tool()
|
||
async def save_block_content(
|
||
case_number: str,
|
||
block_id: str,
|
||
content: str,
|
||
) -> str:
|
||
"""שמירת בלוק שנכתב ע"י Claude Code ב-DB."""
|
||
return await drafting.save_block_content(case_number, block_id, content)
|
||
|
||
|
||
@mcp.tool()
|
||
async def validate_decision(case_number: str) -> str:
|
||
"""בדיקת QA — 6 בדיקות איכות על ההחלטה. אם בדיקה קריטית נכשלת — ייצוא חסום."""
|
||
return await drafting.validate_decision(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def export_docx(case_number: str, output_path: str = "") -> str:
|
||
"""ייצוא החלטה לקובץ DOCX מעוצב — גופן David, RTL, כותרות, מספור סעיפים."""
|
||
return await drafting.export_docx(case_number, output_path)
|
||
|
||
|
||
@mcp.tool()
|
||
async def extract_appraiser_facts(case_number: str) -> str:
|
||
"""חילוץ תכניות והיתרים מכל השומות בתיק וזיהוי סתירות בין שמאים. הכנה לטיוטת ביניים."""
|
||
return await drafting.extract_appraiser_facts(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def get_appraiser_facts(case_number: str) -> str:
|
||
"""קריאת עובדות-השמאי שכבר חולצו (facts + סתירות) — ללא חילוץ-מחדש יקר. ה-get המקביל ל-extract_appraiser_facts."""
|
||
return await drafting.get_appraiser_facts(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
||
"""כתיבת ארבעת הבלוקים לטיוטת ביניים (רקע, תכניות+היתרים, טענות, הליכים) — אותו skill וטמפלט."""
|
||
return await drafting.write_interim_draft(case_number, instructions)
|
||
|
||
|
||
@mcp.tool()
|
||
async def export_interim_draft(case_number: str, output_path: str = "") -> str:
|
||
"""ייצוא טיוטת ביניים ל-DOCX — סדר חדש (רקע → תכניות+היתרים → טענות → הליכים), ללא דיון/סיכום."""
|
||
return await drafting.export_interim_draft(case_number, output_path)
|
||
|
||
|
||
@mcp.tool()
|
||
async def apply_user_edit(case_number: str, edit_filename: str) -> str:
|
||
"""רישום עריכה שהעלה המשתמש (עריכה-v*.docx) כמקור האמת החדש — מזריק bookmarks אם חסר."""
|
||
return await drafting.apply_user_edit(case_number, edit_filename)
|
||
|
||
|
||
@mcp.tool()
|
||
async def list_bookmarks(case_number: str) -> str:
|
||
"""רשימת bookmarks הקיימים ב-active_draft של התיק (אנקורים ל-revisions)."""
|
||
return await drafting.list_bookmarks(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def revise_draft(case_number: str, revisions_json: str,
|
||
author: str = "מערכת AI") -> str:
|
||
"""החלת revisions (Track Changes) על ה-active_draft, יוצר טיוטה-v{N+1}.docx חדשה."""
|
||
return await drafting.revise_draft(case_number, revisions_json, author)
|
||
|
||
|
||
@mcp.tool()
|
||
async def analyze_style(appeal_subtype: str = "") -> str:
|
||
"""ניתוח סגנון על קורפוס ההחלטות של דפנה. מחלץ ושומר דפוסי כתיבה. סוג ערר: building_permit / betterment_levy / compensation_197 (ריק = הכל)."""
|
||
return await drafting.analyze_style(appeal_subtype)
|
||
|
||
|
||
@mcp.tool()
|
||
async def write_block(
|
||
case_number: str,
|
||
block_id: str,
|
||
instructions: str = "",
|
||
) -> str:
|
||
"""כתיבת בלוק יחיד בהחלטה: block-alef עד block-yod-bet. שומר ב-DB."""
|
||
return await drafting.write_block(case_number, block_id, instructions)
|
||
|
||
|
||
@mcp.tool()
|
||
async def write_all_blocks(
|
||
case_number: str,
|
||
start_from: str = "block-alef",
|
||
instructions: str = "",
|
||
) -> str:
|
||
"""כתיבת כל הבלוקים בהחלטה, בלוק אחרי בלוק. שומר כל בלוק מיד."""
|
||
return await drafting.write_all_blocks(case_number, start_from, instructions)
|
||
|
||
|
||
# Workflow
|
||
@mcp.tool()
|
||
async def workflow_status(case_number: str) -> str:
|
||
"""סטטוס תהליך עבודה מלא לתיק."""
|
||
return await workflow.workflow_status(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def get_metrics(case_number: str = "") -> str:
|
||
"""מדדי הצלחה — KPIs לתיק ספציפי או דשבורד כולל. ריק = דשבורד."""
|
||
return await workflow.get_metrics(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def processing_status() -> str:
|
||
"""סטטוס כללי - מספר תיקים, מסמכים, chunks."""
|
||
return await workflow.processing_status()
|
||
|
||
|
||
# Outcome & Brainstorming
|
||
@mcp.tool()
|
||
async def set_outcome(
|
||
case_number: str,
|
||
outcome: str,
|
||
reasoning: str = "",
|
||
) -> str:
|
||
"""הזנת תוצאה לתיק: rejection (דחייה), partial_acceptance (קבלה חלקית), full_acceptance (קבלה מלאה). ערכי-legacy ממופים. אם אין נימוק — מפעיל סיעור מוחות."""
|
||
return await workflow.set_outcome(case_number, outcome, reasoning)
|
||
|
||
|
||
@mcp.tool()
|
||
async def brainstorm_directions(case_number: str) -> str:
|
||
"""סיעור מוחות — הצגת טענות מרכזיות והצעת 2-3 כיוונים אפשריים לנימוק ההחלטה."""
|
||
return await workflow.brainstorm_directions(case_number)
|
||
|
||
|
||
@mcp.tool()
|
||
async def approve_direction(
|
||
case_number: str,
|
||
direction_index: int = 0,
|
||
additional_notes: str = "",
|
||
) -> str:
|
||
"""אישור כיוון — יוצר מסמך כיוון מאושר. חובה לפני כתיבת דיון (בלוק י)."""
|
||
return await workflow.approve_direction(case_number, direction_index, additional_notes)
|
||
|
||
|
||
@mcp.tool()
|
||
async def ingest_final_version(
|
||
case_number: str,
|
||
file_path: str = "",
|
||
final_text: str = "",
|
||
) -> str:
|
||
"""קליטת גרסה סופית שדפנה חתמה — השוואה לטיוטה וחילוץ לקחים לשיפור עתידי."""
|
||
return await workflow.ingest_final_version(case_number, file_path, final_text)
|
||
|
||
|
||
@mcp.tool()
|
||
async def internal_decision_migrate(
|
||
source: str = "both",
|
||
dry_run: bool = True,
|
||
) -> str:
|
||
"""העברת החלטות ועדת ערר קיימות לקורפוס הפנימי (פעולת admin).
|
||
|
||
source: 'style_corpus' | 'external_corpus' | 'both'
|
||
dry_run: אם true — מציג מה יקרה ללא כתיבה
|
||
"""
|
||
import json as _json
|
||
from legal_mcp.services import internal_decisions as int_svc
|
||
if source not in {"style_corpus", "external_corpus", "both"}:
|
||
return "source חייב להיות style_corpus / external_corpus / both"
|
||
results: dict = {}
|
||
if source in {"style_corpus", "both"}:
|
||
results["style_corpus"] = await int_svc.migrate_from_style_corpus(dry_run=dry_run)
|
||
if source in {"external_corpus", "both"}:
|
||
results["external_corpus"] = await int_svc.migrate_from_external_corpus(dry_run=dry_run)
|
||
return _json.dumps(results, ensure_ascii=False, indent=2)
|
||
|
||
|
||
@mcp.tool()
|
||
async def internal_decision_enrich(
|
||
dry_run: bool = True,
|
||
) -> str:
|
||
"""העשרת החלטות שהומגרו (חד-פעמי): תיקון מספר ערר + שם + תאריך + תור להלכות.
|
||
|
||
dry_run=True — מציג כמה רשומות יטופלו ללא כתיבה.
|
||
dry_run=False — מריץ בפועל: metadata extraction (תיקון case_number/case_name/date) ואחר כך תור חילוץ הלכות.
|
||
"""
|
||
import json as _json
|
||
from legal_mcp.services import internal_decisions as int_svc
|
||
result = await int_svc.enrich_migrated_entries(dry_run=dry_run)
|
||
return _json.dumps(result, ensure_ascii=False, indent=2)
|
||
|
||
|
||
@mcp.tool()
|
||
async def internal_decision_upload(
|
||
file_path: str,
|
||
case_number: str,
|
||
chair_name: str,
|
||
district: str,
|
||
case_name: str = "",
|
||
court: str = "",
|
||
decision_date: str = "",
|
||
practice_area: str = "",
|
||
appeal_subtype: str = "",
|
||
subject_tags: list[str] | None = None,
|
||
summary: str = "",
|
||
is_binding: bool = False,
|
||
) -> str:
|
||
"""העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי.
|
||
|
||
שדות חובה: file_path, case_number, chair_name, district.
|
||
שמירת ההחלטה עוברת דרך ingest_internal_decision — תויג source_kind='internal_committee' אוטומטית.
|
||
district תקין: ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי.
|
||
|
||
בניגוד ל-precedent_library_upload (שתמיד שומר external_upload),
|
||
הכלי הזה הוא הנתיב המוסמך להחלטות ועדת ערר ומכריח chair_name+district.
|
||
"""
|
||
return await int_tools.internal_decision_upload(
|
||
file_path=file_path,
|
||
case_number=case_number,
|
||
chair_name=chair_name,
|
||
district=district,
|
||
case_name=case_name,
|
||
court=court,
|
||
decision_date=decision_date,
|
||
practice_area=practice_area,
|
||
appeal_subtype=appeal_subtype,
|
||
subject_tags=subject_tags,
|
||
summary=summary,
|
||
is_binding=is_binding,
|
||
)
|
||
|
||
|
||
# ── Missing precedents (TaskMaster #35) ───────────────────────────
|
||
|
||
|
||
@mcp.tool()
|
||
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:
|
||
"""תיעוד פסיקה שצוטטה בכתבי הטענות אך אינה בקורפוס.
|
||
|
||
שימוש: סוכן המחקר (legal-researcher) קורא לזה כשהוא מזהה ציטוט שלא
|
||
ניתן לאמת מול הקורפוס. הרשומה נשארת 'open' עד שהיו"ר מעלה את הפסיקה.
|
||
cited_by_party: appellant / respondent / committee / permit_applicant / unknown.
|
||
דה-דופ אוטומטי: ציטוט+תיק זהים → מחזיר את הרשומה הקיימת.
|
||
"""
|
||
return await mp_tools.missing_precedent_create(
|
||
citation=citation,
|
||
case_number=case_number,
|
||
cited_in_document_id=cited_in_document_id,
|
||
cited_by_party=cited_by_party,
|
||
cited_by_party_name=cited_by_party_name,
|
||
legal_topic=legal_topic,
|
||
legal_issue=legal_issue,
|
||
claim_quote=claim_quote,
|
||
case_name=case_name,
|
||
notes=notes,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def missing_precedent_list(
|
||
case_number: str = "",
|
||
status: str = "open",
|
||
legal_topic: str = "",
|
||
limit: int = 50,
|
||
) -> str:
|
||
"""רשימת פסיקות חסרות לתיק או בכלל. status: open/uploaded/closed/irrelevant.
|
||
|
||
שימוש: היו"ר רואה מה ממתין להעלאה; הסוכן מאשר שלא יוצר כפילויות.
|
||
"""
|
||
return await mp_tools.missing_precedent_list(
|
||
case_number=case_number,
|
||
status=status,
|
||
legal_topic=legal_topic,
|
||
limit=_clamp_limit(limit),
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def missing_precedent_close(
|
||
id: str,
|
||
linked_case_law_id: str = "",
|
||
notes: str = "",
|
||
status: str = "closed",
|
||
) -> str:
|
||
"""סגירת רשומת פסיקה חסרה לאחר העלאה לקורפוס.
|
||
|
||
status: closed (הועלה ונקשר) / uploaded (הועלה, ממתין לקישור) /
|
||
irrelevant (היו"ר החליט שזה לא רלוונטי לקורפוס).
|
||
"""
|
||
return await mp_tools.missing_precedent_close(
|
||
id=id,
|
||
linked_case_law_id=linked_case_law_id,
|
||
notes=notes,
|
||
status=status,
|
||
)
|
||
|
||
|
||
# ── Internal citations graph (TaskMaster #34) ─────────────────────
|
||
|
||
|
||
@mcp.tool()
|
||
async def extract_internal_citations(
|
||
case_law_id: str = "",
|
||
chair_name: str = "",
|
||
limit: int = 0,
|
||
) -> str:
|
||
"""חילוץ ציטוטים פנימיים מהחלטות ועדת ערר ושמירה ב-citation graph.
|
||
|
||
משתמש בדפוסי regex עבריים ("ונפנה ל…", "כפי שקבעתי…", "ראה החלטתי…")
|
||
לזיהוי הפניות בין החלטות. אם case_law_id סופק — מריץ על שורה אחת
|
||
(שימושי אחרי upload). אם chair_name סופק — מריץ על כל ההחלטות של
|
||
אותו יו"ר. אם שניהם ריקים — מריץ על כל ה-internal_committee corpus.
|
||
|
||
איידמפוטנטי: ניתן להריץ שוב ושוב בלי כפילויות. ציטוטים שמופנים
|
||
להחלטות שעדיין לא בקורפוס נשמרים כ-unlinked (cited_case_law_id=NULL)
|
||
ויראו ב-list_internal_citations כשהיו"ר יחליט אם להעלות אותן.
|
||
"""
|
||
return await cit_tools.extract_internal_citations(
|
||
case_law_id=case_law_id,
|
||
chair_name=chair_name,
|
||
limit=limit,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def list_internal_citations(
|
||
case_law_id: str = "",
|
||
linked_only: bool = False,
|
||
limit: int = 50,
|
||
) -> str:
|
||
"""רשימת ציטוטים יוצאים מהחלטה (מה ההחלטה מצטטת).
|
||
|
||
משתמש לקבלת תמונה של בסיס הפסיקה שהחלטה הסתמכה עליו.
|
||
linked_only=True מסנן רק ציטוטים שזוהו ב-case_law של הקורפוס.
|
||
"""
|
||
return await cit_tools.list_internal_citations(
|
||
case_law_id=case_law_id,
|
||
linked_only=linked_only,
|
||
limit=_clamp_limit(limit),
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def list_incoming_citations(
|
||
case_law_id: str = "",
|
||
limit: int = 50,
|
||
) -> str:
|
||
"""רשימת ציטוטים נכנסים אל החלטה (אילו החלטות מצטטות אותה).
|
||
|
||
שימוש: רוצים לדעת אילו החלטות של דפנה (או של ועדות אחרות) הסתמכו
|
||
על פסק דין מסוים — מעבירים את ה-case_law_id של פסק הדין.
|
||
"""
|
||
return await cit_tools.list_incoming_citations(
|
||
case_law_id=case_law_id,
|
||
limit=_clamp_limit(limit),
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def record_chair_feedback(
|
||
case_number: str,
|
||
feedback_text: str,
|
||
block_id: str = "block-yod",
|
||
category: str = "missing_content",
|
||
lesson_extracted: str = "",
|
||
) -> str:
|
||
"""תיעוד הערת יו"ר (דפנה) על טיוטת החלטה — חסר, שגיאה, סגנון."""
|
||
return await workflow.record_chair_feedback(
|
||
case_number, feedback_text, block_id, category, lesson_extracted,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def list_chair_feedback(
|
||
case_number: str = "",
|
||
category: str = "",
|
||
unresolved_only: bool = True,
|
||
limit: int = 100,
|
||
) -> str:
|
||
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
||
return await workflow.list_chair_feedback(case_number, category, unresolved_only, _clamp_limit(limit))
|
||
|
||
|
||
@mcp.tool()
|
||
async def halacha_corroboration(halacha_id: str) -> dict:
|
||
"""החזר את ה-corroboration של הלכה: הציטוטים שמתקפים אותה, הטיפול, וסיכום (X11, read-only)."""
|
||
from uuid import UUID
|
||
from legal_mcp.services import corroboration as cor, db
|
||
links = await db.list_corroboration_for_halacha(UUID(halacha_id))
|
||
agg = cor.aggregate(
|
||
[{"source_id": (l["citing_case_law_id"] or l["citing_decision_id"] or ""), "treatment": l["treatment"]} for l in links]
|
||
)
|
||
return {"halacha_id": halacha_id, "summary": agg, "citations": links}
|
||
|
||
|
||
@mcp.tool()
|
||
async def corroboration_rebuild(case_law_id: str = "") -> dict:
|
||
"""בנה/רענן את ה-corroboration ויישם אישור-אוטומטי. ריק = כל הקורפוס (backfill);
|
||
מזהה-תקדים = תקדים בודד. כותב halacha_citation_corroboration ומעדכן review_status
|
||
(corroborated→approved, overruled→pending_review). X11 Phase 2."""
|
||
from legal_mcp.services import corroboration as cor
|
||
if case_law_id.strip():
|
||
return await cor.build_for_precedent(case_law_id.strip())
|
||
return await cor.build_all()
|
||
|
||
|
||
def main():
|
||
mcp.run(transport="stdio")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|