Add pre-ruling interim draft (טיוטת ביניים) for appeals committee
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m26s

Lets the chair generate a partial decision DOCX before the discussion-and-
ruling block is decided. Same template, skill and DOCX styling as the final
decision (David, RTL, bookmarks) — only the block selection and order differ:
רקע (ו) → תכניות+היתרים (ט) → טענות (ז) → הליכים (ח). The opening (ה),
ruling (י), summary (יא), and signatures (יב) are omitted.

- New appraiser_facts table + CRUD + conflict detection in db.py (V5 schema).
  Conflict = same plan/permit identifier reported differently by 2+ appraisers.
- New appraiser_facts_extractor service: per-appraisal Claude extraction of
  plans + permits with raw quotes and page numbers.
- block-tet prompt extended with a permits sub-section sourced from the
  extracted facts, plus an explicit instruction to flag inter-appraiser
  conflicts in neutral wording without resolving them (deferred to block-yod).
- block-chet prompt extended with a post-hearing materials context sourced
  from documents.metadata.is_post_hearing.
- docx_exporter.export_decision now accepts mode='interim' which reorders
  the blocks per the chair's mental model and writes
  טיוטת-ביניים-v{N}.docx (versioned independently of regular drafts).
- 3 new MCP tools: extract_appraiser_facts, write_interim_draft,
  export_interim_draft. write_interim_draft auto-runs extraction if the
  appraiser_facts table is empty for the case.
This commit is contained in:
2026-04-18 13:28:04 +00:00
parent 2b40e02a65
commit c619c22a51
7 changed files with 731 additions and 17 deletions

View File

@@ -416,6 +416,130 @@ async def export_docx(case_number: str, output_path: str = "") -> str:
}, ensure_ascii=False, indent=2)
# ── Interim draft (pre-ruling) ────────────────────────────────────
# Blocks written for the interim draft, in display order.
# This is the same content the chair sees in the final decision (same template,
# same skill, same prompts) — minus opening, ruling, summary, signatures.
_INTERIM_BLOCKS = ["block-vav", "block-tet", "block-zayin", "block-chet"]
async def extract_appraiser_facts(case_number: str) -> str:
"""חילוץ תכניות והיתרים מכל השומות בתיק וזיהוי סתירות בין שמאים.
משמש כהכנה לטיוטת ביניים: בלוק ט (תכניות חלות) זקוק לעובדות מובנות
כדי לפרט תת-פרק היתרים ולסמן סתירות בנוסח ניטרלי.
Args:
case_number: מספר תיק הערר
"""
from legal_mcp.services import appraiser_facts_extractor
case = await db.get_case_by_number(case_number)
if not case:
return json.dumps({"status": "error",
"message": f"תיק {case_number} לא נמצא."},
ensure_ascii=False, indent=2)
case_id = UUID(case["id"])
try:
result = await appraiser_facts_extractor.extract_appraiser_facts(case_id)
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
except Exception as e:
return json.dumps({"status": "error", "message": str(e)},
ensure_ascii=False, indent=2)
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
"""כתיבת ארבעת הבלוקים לטיוטת ביניים: רקע (ו), תכניות+היתרים (ט),
טענות הצדדים (ז), הליכים (ח). אם לא חולצו עובדות שמאיות עדיין —
מריץ extract_appraiser_facts קודם כדי שבלוק ט יקבל פרק היתרים תקף.
הבלוקים נכתבים באותו skill, אותם prompts ואותו טמפלט כמו בטיוטה רגילה —
הסדר משתנה רק בעת הייצוא ל-DOCX (ראה export_interim_draft).
Args:
case_number: מספר תיק הערר
instructions: הנחיות נוספות (לכל הבלוקים)
"""
from legal_mcp.services import appraiser_facts_extractor, block_writer
case = await db.get_case_by_number(case_number)
if not case:
return json.dumps({"status": "error",
"message": f"תיק {case_number} לא נמצא."},
ensure_ascii=False, indent=2)
case_id = UUID(case["id"])
# Make sure appraiser facts exist before writing block-tet (which depends on them).
facts = await db.list_appraiser_facts(case_id)
facts_run: dict | None = None
if not facts:
try:
facts_run = await appraiser_facts_extractor.extract_appraiser_facts(case_id)
except Exception as e:
facts_run = {"status": "error", "message": str(e)}
results = []
for bid in _INTERIM_BLOCKS:
try:
r = await block_writer.write_and_store_block(case_id, bid, instructions)
results.append({
"block_id": bid,
"title": r["title"],
"word_count": r["word_count"],
"status": "completed",
})
except Exception as e:
results.append({
"block_id": bid,
"status": "error",
"error": str(e),
})
return json.dumps({
"status": "completed",
"blocks": results,
"appraiser_facts_run": facts_run,
"total_words": sum(r.get("word_count", 0) for r in results),
"completed": sum(1 for r in results if r["status"] == "completed"),
}, default=str, ensure_ascii=False, indent=2)
async def export_interim_draft(case_number: str, output_path: str = "") -> str:
"""ייצוא טיוטת ביניים ל-DOCX — אותו עיצוב של טיוטה רגילה (David, RTL,
bookmarks), אבל בסדר חדש: רקע → תכניות+היתרים → טענות → הליכים, ללא
דיון/סיכום/חתימות. שם הקובץ: טיוטת-ביניים-v{N}.docx.
Args:
case_number: מספר תיק הערר
output_path: נתיב לשמירה (אופציונלי)
"""
from legal_mcp.services import docx_exporter
case = await db.get_case_by_number(case_number)
if not case:
return json.dumps({"status": "error",
"message": f"תיק {case_number} לא נמצא."},
ensure_ascii=False, indent=2)
case_id = UUID(case["id"])
try:
path = await docx_exporter.export_decision(
case_id, output_path or None, mode="interim",
)
await db.set_active_draft_path(case_id, path)
return json.dumps({
"status": "completed",
"mode": "interim",
"path": path,
"active_draft_path": path,
"message": f"טיוטת ביניים נוצרה: {path}",
}, ensure_ascii=False, indent=2)
except ValueError as e:
return json.dumps({"status": "error", "message": str(e)},
ensure_ascii=False, indent=2)
async def apply_user_edit(case_number: str, edit_filename: str) -> str:
"""רישום עריכה שהעלה המשתמש כמקור האמת החדש של התיק.