Add ability to mark case_law records as related (e.g. same appeal
through ועדת ערר → מנהלי → עליון):
- DB: case_law_relations join table (bidirectional, V11 migration)
- DB CRUD: add/remove/get_case_law_relations
- Service: get_precedent() now returns related_cases[]
- MCP: precedent_link_cases + precedent_unlink_cases tools
- REST: POST/DELETE /api/precedent-library/{id}/relations
- UI: RelatedCasesSection on detail page with search dialog and unlink
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
695 lines
25 KiB
Python
695 lines
25 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,
|
||
)
|
||
|
||
|
||
# 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,
|
||
)
|
||
|
||
|
||
@mcp.tool()
|
||
async def case_list(status: str = "", limit: int = 50) -> str:
|
||
"""רשימת תיקי ערר. סינון אופציונלי לפי סטטוס (new/in_progress/drafted/reviewed/final)."""
|
||
return await cases.case_list(status, 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."""
|
||
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, 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 = "",
|
||
limit: int = 100,
|
||
) -> str:
|
||
"""רשימת הפסיקה בקורפוס הסמכותי, עם פילטרים."""
|
||
return await plib.precedent_library_list(
|
||
practice_area, court, precedent_level, source_type, search, 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_process_pending(kind: str = "metadata", limit: int = 20) -> str:
|
||
"""ריקון תור בקשות חילוץ שנשלחו מ-UI. kind: 'metadata' או 'halacha'. מריץ extractor מקומית עם CLI על כל פריט בתור, ומנקה את הסימון אחרי הצלחה."""
|
||
return await plib.precedent_process_pending(kind, 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, 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(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)
|
||
|
||
|
||
# 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, 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, 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, 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,
|
||
) -> str:
|
||
"""חיפוש בהחלטות ועדות ערר לתכנון ובנייה (כל המחוזות).
|
||
|
||
מחזיר החלטות מהקורפוס הפנימי של ועדות הערר — נפרד מפסיקת בתי המשפט.
|
||
השתמש בו במקביל ל-search_precedent_library להצגת שתי שכבות נפרדות.
|
||
|
||
Args:
|
||
query: שאילתת חיפוש בעברית
|
||
practice_area: rishuy_uvniya / betterment_levy / compensation_197
|
||
appeal_subtype: סינון לפי תת-סוג ערר
|
||
district: מחוז — ירושלים / מרכז / תל אביב / צפון / דרום / ארצי. ריק = כל המחוזות
|
||
chair_name: שם יו"ר הוועדה לסינון. ריק = כל היו"רים
|
||
limit: מספר תוצאות מקסימלי
|
||
include_halachot: האם לכלול הלכות שחולצו
|
||
"""
|
||
return await search.search_internal_decisions(
|
||
query, practice_area, appeal_subtype, district, chair_name, limit, include_halachot,
|
||
)
|
||
|
||
|
||
# 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 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:
|
||
"""הזנת תוצאה לתיק: rejected (דחייה), accepted (קבלה), partial (קבלה חלקית). אם אין נימוק — מפעיל סיעור מוחות."""
|
||
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 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,
|
||
) -> str:
|
||
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
||
return await workflow.list_chair_feedback(case_number, category, unresolved_only)
|
||
|
||
|
||
def main():
|
||
mcp.run(transport="stdio")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|