Add pre-ruling interim draft (טיוטת ביניים) for appeals committee
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m26s
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:
@@ -165,9 +165,10 @@ BLOCK_PROMPTS = {
|
||||
"block-chet": """כתוב את בלוק ההליכים (בלוק ח, "ההליכים בפני ועדת הערר") של החלטת ועדת ערר.
|
||||
|
||||
## כללים:
|
||||
- תיעוד כרונולוגי: דיון → סיור → השלמות טיעון → החלטות ביניים
|
||||
- תיעוד כרונולוגי: דיון → סיור → השלמות טיעון → משא-ומתן לפשרה (אם היה) → החלטות ביניים
|
||||
- תאריכים מדויקים
|
||||
- תוכן כל השלמת טיעון בסעיף נפרד
|
||||
- אם בדיון עלו נקודות חדשות או הובהרו סוגיות משפטיות — ציין זאת במפורש בסעיף נפרד
|
||||
- תוכן כל השלמת טיעון/הצעת פשרה בסעיף נפרד עם תאריך
|
||||
- סמן תמונות מסיור: [📷 צילום מסיור]
|
||||
- אין ניתוח או הערכה
|
||||
- מספור רציף
|
||||
@@ -175,24 +176,41 @@ BLOCK_PROMPTS = {
|
||||
## פרטי התיק:
|
||||
{case_context}
|
||||
|
||||
## מסמכים שהוגשו לאחר הדיון (אם יש):
|
||||
{post_hearing_context}
|
||||
|
||||
## חומרי מקור:
|
||||
{source_context}""",
|
||||
|
||||
"block-tet": """כתוב את בלוק התכניות החלות (בלוק ט) של החלטת ועדת ערר.
|
||||
"block-tet": """כתוב את בלוק התכניות החלות (בלוק ט) של החלטת ועדת ערר, **כולל תת-פרק היתרים**.
|
||||
|
||||
## כללים:
|
||||
- ציטוט ישיר מהוראות תכנית עם **הדגשה** של מילים מכריעות
|
||||
- מבנה הירכי: תכניות ארציות → מחוזיות → מקומיות
|
||||
## מבנה נדרש:
|
||||
1. **תכניות חלות** — מבנה הירכי: תכניות ארציות → מחוזיות → מקומיות. ציטוט ישיר מהוראות תכנית עם **הדגשה** של מילים מכריעות.
|
||||
2. **תת-פרק היתרים** — כותרת משנה "היתרים" (או "היתרי בנייה שניתנו במקרקעין"). פירוט ההיתרים הרלוונטיים על פי השומות שהוגשו לתיק.
|
||||
|
||||
## כללי ציון סתירות בין שמאים (קריטי):
|
||||
- אם שני שמאים או יותר מסרו מידע שונה על אותה תכנית או היתר — חובה לסמן זאת במפורש בנוסח ניטרלי, למשל:
|
||||
> "יצוין כי השמאי X ציין כי תכנית פלונית חלה על המקרקעין במלואה, בעוד השמאי Y סבר כי חלקה של התכנית בלבד חל"
|
||||
- אין להכריע בסתירה בבלוק זה — ההכרעה (אם נדרשת) תבוא בבלוק י.
|
||||
- אם אין סתירה — אין להזכיר זאת.
|
||||
|
||||
## כללים נוספים:
|
||||
- אין ניתוח מעמיק (→ בלוק י), אין הכרעה בין פרשנויות
|
||||
- מספור רציף
|
||||
- בלוק אופציונלי — כתוב רק אם יש מורכבות תכנונית
|
||||
- אם אין שומות בתיק — דווח רק על תכניות שזוהו ממסמכים אחרים, וציין במשפט אחד שלא הוגשו שומות
|
||||
|
||||
## פרטי התיק:
|
||||
{case_context}
|
||||
|
||||
## תכניות שזוהו:
|
||||
## תכניות שזוהו (ממטא-דאטה של מסמכים):
|
||||
{plans_context}
|
||||
|
||||
## עובדות שמאיות שחולצו (תכניות + היתרים, פרק לכל שמאי):
|
||||
{appraiser_facts_context}
|
||||
|
||||
## סתירות שזוהו בין שמאים (חובה לסמן בנוסח):
|
||||
{appraiser_conflicts_context}
|
||||
|
||||
## חומרי מקור:
|
||||
{source_context}""",
|
||||
|
||||
@@ -301,6 +319,9 @@ async def write_block(
|
||||
precedents_context = await _build_precedents_context(case_id, block_id)
|
||||
style_context = await _build_style_context()
|
||||
discussion_context = await _build_previous_blocks_context(case_id, decision)
|
||||
appraiser_facts_context = await _build_appraiser_facts_context(case_id)
|
||||
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
||||
post_hearing_context = await _build_post_hearing_context(case_id)
|
||||
|
||||
outcome = (decision or {}).get("outcome", "rejected")
|
||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||
@@ -332,6 +353,9 @@ async def write_block(
|
||||
structure_guidance=structure_guidance,
|
||||
content_checklist=content_checklist,
|
||||
methodology_guidance=methodology_guidance,
|
||||
appraiser_facts_context=appraiser_facts_context,
|
||||
appraiser_conflicts_context=appraiser_conflicts_context,
|
||||
post_hearing_context=post_hearing_context,
|
||||
)
|
||||
|
||||
# Restructure: sources first, then instructions
|
||||
@@ -478,6 +502,112 @@ async def _build_plans_context(case_id: UUID) -> str:
|
||||
return "(לא זוהו תכניות)"
|
||||
|
||||
|
||||
async def _build_appraiser_facts_context(case_id: UUID) -> str:
|
||||
"""Group appraiser_facts by appraiser, then list each appraiser's plans+permits."""
|
||||
facts = await db.list_appraiser_facts(case_id)
|
||||
if not facts:
|
||||
return "(לא חולצו עובדות שמאיות. הרץ extract_appraiser_facts.)"
|
||||
|
||||
by_appraiser: dict[str, dict[str, list[dict]]] = {}
|
||||
for f in facts:
|
||||
bucket = by_appraiser.setdefault(f["appraiser_name"], {"plan": [], "permit": []})
|
||||
bucket[f["fact_type"]].append(f)
|
||||
|
||||
lines: list[str] = []
|
||||
for name in sorted(by_appraiser.keys()):
|
||||
lines.append(f"\n### {name}")
|
||||
for label, key in (("תכניות", "plan"), ("היתרים", "permit")):
|
||||
items = by_appraiser[name][key]
|
||||
if not items:
|
||||
continue
|
||||
lines.append(f"**{label}:**")
|
||||
for item in items:
|
||||
details = item.get("details") or {}
|
||||
ident = item["identifier"]
|
||||
scope = (details.get("scope") or "").strip()
|
||||
date_s = (details.get("date") or "").strip()
|
||||
status = (details.get("status") or "").strip()
|
||||
quote = (details.get("raw_quote") or "").strip()
|
||||
bits = [ident]
|
||||
if date_s:
|
||||
bits.append(f"תאריך: {date_s}")
|
||||
if status:
|
||||
bits.append(f"סטטוס: {status}")
|
||||
if scope:
|
||||
bits.append(f"היקף: {scope}")
|
||||
line = " | ".join(bits)
|
||||
if quote:
|
||||
line += f"\n ציטוט: \"{quote[:200]}\""
|
||||
lines.append(f"- {line}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def _build_appraiser_conflicts_context(case_id: UUID) -> str:
|
||||
"""Render conflict groups so the prompt can quote them in the body."""
|
||||
conflicts = await db.detect_appraiser_conflicts(case_id)
|
||||
if not conflicts:
|
||||
return "(אין סתירות בין שמאים)"
|
||||
|
||||
type_label = {"plan": "תכנית", "permit": "היתר"}
|
||||
lines: list[str] = []
|
||||
for c in conflicts:
|
||||
lines.append(
|
||||
f"\n### סתירה — {type_label.get(c['fact_type'], c['fact_type'])}: {c['identifier']}"
|
||||
)
|
||||
for entry in c["entries"]:
|
||||
details = entry.get("details") or {}
|
||||
scope = (details.get("scope") or "").strip()
|
||||
status = (details.get("status") or "").strip()
|
||||
quote = (details.get("raw_quote") or "").strip()
|
||||
parts = [f"**{entry['appraiser_name']}**"]
|
||||
if status:
|
||||
parts.append(f"סטטוס: {status}")
|
||||
if scope:
|
||||
parts.append(f"היקף: {scope}")
|
||||
line = " | ".join(parts)
|
||||
if quote:
|
||||
line += f"\n ציטוט: \"{quote[:200]}\""
|
||||
lines.append(f"- {line}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def _build_post_hearing_context(case_id: UUID) -> str:
|
||||
"""List documents flagged as submitted after the hearing.
|
||||
|
||||
Convention: documents.metadata.is_post_hearing == True.
|
||||
"""
|
||||
docs = await db.list_documents(case_id)
|
||||
items: list[dict] = []
|
||||
for d in docs:
|
||||
meta = d.get("metadata") or {}
|
||||
if isinstance(meta, str):
|
||||
meta = json.loads(meta)
|
||||
if not meta.get("is_post_hearing"):
|
||||
continue
|
||||
items.append({
|
||||
"title": d.get("title", ""),
|
||||
"doc_type": d.get("doc_type", ""),
|
||||
"submitted_on": meta.get("submitted_on", ""),
|
||||
"kind": meta.get("post_hearing_kind", ""), # "supplementary_brief" | "settlement_proposal" | ...
|
||||
})
|
||||
|
||||
if not items:
|
||||
return "(לא הוגשו מסמכים לאחר הדיון, או שהם לא סומנו כ-post_hearing)"
|
||||
|
||||
lines: list[str] = []
|
||||
for it in items:
|
||||
meta_bits = []
|
||||
if it["submitted_on"]:
|
||||
meta_bits.append(f"הוגש: {it['submitted_on']}")
|
||||
if it["kind"]:
|
||||
meta_bits.append(f"סוג: {it['kind']}")
|
||||
if it["doc_type"]:
|
||||
meta_bits.append(f"doc_type={it['doc_type']}")
|
||||
meta_str = f" ({', '.join(meta_bits)})" if meta_bits else ""
|
||||
lines.append(f"- {it['title']}{meta_str}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def _build_precedents_context(case_id: UUID, block_id: str) -> str:
|
||||
"""Search for similar precedent paragraphs from other decisions and case law."""
|
||||
parts = []
|
||||
@@ -654,6 +784,9 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
|
||||
precedents_context = await _build_precedents_context(case_id, block_id)
|
||||
style_context = await _build_style_context()
|
||||
discussion_context = await _build_previous_blocks_context(case_id, decision)
|
||||
appraiser_facts_context = await _build_appraiser_facts_context(case_id)
|
||||
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
||||
post_hearing_context = await _build_post_hearing_context(case_id)
|
||||
|
||||
outcome = (decision or {}).get("outcome", "rejected")
|
||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||
@@ -681,6 +814,9 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
|
||||
structure_guidance=structure_guidance,
|
||||
content_checklist=content_checklist,
|
||||
methodology_guidance=methodology_guidance,
|
||||
appraiser_facts_context=appraiser_facts_context,
|
||||
appraiser_conflicts_context=appraiser_conflicts_context,
|
||||
post_hearing_context=post_hearing_context,
|
||||
)
|
||||
|
||||
if instructions:
|
||||
|
||||
Reference in New Issue
Block a user