Edit document doc_type and appraiser side from the case UI
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
Until now changing a document's doc_type required a manual SQL update.
Adds an inline editor on the document badge so the chair can retag
without leaving the case page, and threads an appraiser_side tag
(committee / appellant / deciding) through the appraisal pipeline so
betterment-levy cases — which usually have 2-3 appraisers — render
conflicts with the deciding appraiser's view marked as governing.
Backend
- New appraiser_facts.appraiser_side column (V5.1) populated from
documents.metadata.appraiser_side at extraction time.
- extract_appraiser_facts now returns status='sides_missing' with the
list of untagged appraisals instead of running with empty side
labels — chair must tag every appraisal first via the UI.
- Conflict detection orders entries committee → appellant → deciding so
the deciding appraiser appears last; block-tet's prompt instructs the
writer to phrase the deciding appraiser's view as the governing
factual finding ("ואולם, השמאי המכריע קבע...").
- New PATCH /api/cases/{n}/documents/{doc_id} (Pydantic model with
whitelist validation) and matching document_update MCP tool. Both
merge appraiser_side into metadata JSONB instead of touching the
schema.
UI
- New shared doc-types module exports the canonical 11 doc_type
options plus the 3 appraiser-side options; both upload-sheet and
the document badge now read from it instead of duplicating Hebrew
labels.
- New DocumentTypeEditor renders a Popover off the doc-type Badge
with two Selects. The save button stays disabled while doc_type is
appraisal but no side has been picked, mirroring the backend
enforcement so the user finds out before triggering extraction.
- usePatchDocument React-Query mutation invalidates the case detail
on success so the badge updates without a manual refresh.
This commit is contained in:
@@ -4,11 +4,16 @@
|
||||
לאפשר זיהוי אוטומטי של סתירות בין שמאים שונים על אותו זיהוי (תכנית או היתר).
|
||||
|
||||
שמירה ב-DB: טבלת appraiser_facts (case_id, document_id, appraiser_name,
|
||||
fact_type, identifier, details JSONB, page_number).
|
||||
appraiser_side, fact_type, identifier, details JSONB, page_number).
|
||||
|
||||
Precondition: כל מסמך doc_type='appraisal' חייב להיות מתויג עם
|
||||
metadata.appraiser_side מתוך {committee, appellant, deciding}. החילוץ עוצר
|
||||
ומחזיר status='sides_missing' אם יש מסמכים לא מתויגים.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
@@ -17,6 +22,13 @@ from legal_mcp.services import claude_session, db
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Allowed sides for an appraiser in an appeals committee case.
|
||||
# committee = שמאי הוועדה המקומית
|
||||
# appellant = שמאי העורר / הצד שכנגד הוועדה
|
||||
# deciding = שמאי מכריע
|
||||
VALID_APPRAISER_SIDES = {"committee", "appellant", "deciding"}
|
||||
|
||||
|
||||
EXTRACT_FACTS_PROMPT = """אתה מנתח שומות מקרקעין לטובת ועדת ערר לתכנון ובניה.
|
||||
|
||||
תפקידך: לחלץ מתוך השומה שתי קטגוריות של עובדות אובייקטיביות שעליהן השמאי מבסס את חוות דעתו:
|
||||
@@ -77,6 +89,7 @@ async def extract_facts_from_document(
|
||||
case_id: UUID,
|
||||
document_id: UUID,
|
||||
appraiser_name: str,
|
||||
appraiser_side: str,
|
||||
text: str,
|
||||
) -> list[dict]:
|
||||
"""Extract structured facts from a single appraisal document via Claude Code."""
|
||||
@@ -107,32 +120,63 @@ async def extract_facts_from_document(
|
||||
continue
|
||||
all_facts.append({
|
||||
"appraiser_name": appraiser_name,
|
||||
"appraiser_side": appraiser_side,
|
||||
"fact_type": item["fact_type"],
|
||||
"identifier": _normalize_identifier(ident),
|
||||
"details": item.get("details") or {},
|
||||
"page_number": item.get("page_number"),
|
||||
})
|
||||
|
||||
if all_facts:
|
||||
await db.replace_appraiser_facts(case_id, document_id, all_facts)
|
||||
else:
|
||||
await db.replace_appraiser_facts(case_id, document_id, [])
|
||||
await db.replace_appraiser_facts(case_id, document_id, all_facts)
|
||||
return all_facts
|
||||
|
||||
|
||||
def _doc_metadata(doc: dict) -> dict:
|
||||
metadata = doc.get("metadata") or {}
|
||||
if isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
metadata = {}
|
||||
return metadata if isinstance(metadata, dict) else {}
|
||||
|
||||
|
||||
def _infer_appraiser_name(doc: dict) -> str:
|
||||
"""Best-effort extraction of the appraiser's name from document title/metadata."""
|
||||
metadata = doc.get("metadata") or {}
|
||||
name = metadata.get("appraiser_name") if isinstance(metadata, dict) else None
|
||||
meta = _doc_metadata(doc)
|
||||
name = meta.get("appraiser_name")
|
||||
if name:
|
||||
return name
|
||||
title = doc.get("title", "")
|
||||
return title or f"שמאי (מסמך {doc.get('id', '')[:8]})"
|
||||
|
||||
|
||||
def _get_appraiser_side(doc: dict) -> str:
|
||||
"""Return the tagged side, or '' if not tagged."""
|
||||
return _doc_metadata(doc).get("appraiser_side", "") or ""
|
||||
|
||||
|
||||
def _validate_sides_tagged(appraisals: list[dict]) -> list[dict]:
|
||||
"""Return the subset of appraisals missing a valid appraiser_side tag."""
|
||||
missing: list[dict] = []
|
||||
for doc in appraisals:
|
||||
side = _get_appraiser_side(doc)
|
||||
if side not in VALID_APPRAISER_SIDES:
|
||||
missing.append({
|
||||
"document_id": doc["id"],
|
||||
"title": doc.get("title", ""),
|
||||
"current_side": side,
|
||||
})
|
||||
return missing
|
||||
|
||||
|
||||
async def extract_appraiser_facts(case_id: UUID) -> dict:
|
||||
"""Extract facts from every appraisal document in the case + detect conflicts.
|
||||
|
||||
Blocks if any appraisal is missing metadata.appraiser_side — the chair must
|
||||
tag each one via the UI before extraction runs, so that conflict rendering
|
||||
in block-tet can identify the deciding appraiser's view as authoritative.
|
||||
|
||||
Returns a summary dict ready for serialization back to the caller.
|
||||
"""
|
||||
docs = await db.list_documents(case_id)
|
||||
@@ -146,6 +190,18 @@ async def extract_appraiser_facts(case_id: UUID) -> dict:
|
||||
"conflicts": [],
|
||||
}
|
||||
|
||||
missing_sides = _validate_sides_tagged(appraisals)
|
||||
if missing_sides:
|
||||
return {
|
||||
"status": "sides_missing",
|
||||
"appraisal_count": len(appraisals),
|
||||
"missing": missing_sides,
|
||||
"message": (
|
||||
"חסר תיוג appraiser_side במסמכי שומה. תייג כל שומה דרך ה-UI "
|
||||
"(ועדה / עורר / מכריע) והרץ שוב."
|
||||
),
|
||||
}
|
||||
|
||||
by_doc = []
|
||||
total_facts = 0
|
||||
for doc in appraisals:
|
||||
@@ -160,11 +216,13 @@ async def extract_appraiser_facts(case_id: UUID) -> dict:
|
||||
continue
|
||||
|
||||
appraiser_name = _infer_appraiser_name(doc)
|
||||
appraiser_side = _get_appraiser_side(doc)
|
||||
try:
|
||||
facts = await extract_facts_from_document(
|
||||
case_id=case_id,
|
||||
document_id=UUID(doc["id"]),
|
||||
appraiser_name=appraiser_name,
|
||||
appraiser_side=appraiser_side,
|
||||
text=text,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -183,6 +241,7 @@ async def extract_appraiser_facts(case_id: UUID) -> dict:
|
||||
"document_id": doc["id"],
|
||||
"title": doc.get("title", ""),
|
||||
"appraiser_name": appraiser_name,
|
||||
"appraiser_side": appraiser_side,
|
||||
"status": "completed",
|
||||
"facts_extracted": len(facts),
|
||||
"plans": sum(1 for f in facts if f["fact_type"] == "plan"),
|
||||
|
||||
Reference in New Issue
Block a user