fix(appraiser-facts): route extraction through analyst wakeup (was silent 0)
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m38s

The "חלץ עובדות שמאיות" UI button hit POST /api/cases/{n}/extract-appraiser-facts
which called appraiser_facts_extractor inline — that shells out to the local
`claude` CLI, which is absent in the Coolify container, so every doc errored,
the per-doc try/except swallowed it, and the response was "completed, 0 facts".

Refactored the endpoint to wake the legal-analyst of the correct company via
Paperclip (same pattern as wake_curator_for_final), and surface
extraction_failed instead of "completed" when every doc errored.
This commit is contained in:
2026-05-26 11:02:55 +00:00
parent 7ad995aade
commit 3a05e30c8d
5 changed files with 221 additions and 13 deletions

View File

@@ -61,6 +61,7 @@ from web.paperclip_client import (
reject_interaction as pc_reject_interaction,
respond_to_interaction as pc_respond_to_interaction,
restore_project as pc_restore_project,
wake_analyst_for_appraiser_facts as pc_wake_analyst_for_appraiser_facts,
wake_ceo_agent as pc_wake_ceo,
wake_curator_for_final as pc_wake_curator_for_final,
wake_for_precedent_extraction as pc_wake_for_precedent_extraction,
@@ -3977,28 +3978,74 @@ async def api_patch_document(case_number: str, doc_id: str, req: DocumentPatchRe
@app.post("/api/cases/{case_number}/extract-appraiser-facts")
async def api_extract_appraiser_facts(case_number: str):
"""Run structured extraction of plans + permits from every appraisal
document in the case, and detect conflicts between appraisers.
"""Queue appraiser-fact extraction by waking the legal-analyst agent.
Blocks if any appraisal document is missing metadata.appraiser_side —
the chair must tag every appraisal (committee / appellant / deciding)
before extraction can identify the deciding appraiser's governing view.
The extraction itself calls `claude_session.query_json()`, which shells
out to the local `claude` CLI — present on the agent host, **absent in
this FastAPI container**. So we cannot run the extractor inline here.
Returns the extractor's summary dict as-is. Shape:
{"status": "completed"|"sides_missing"|"no_appraisals", ...}
Instead we delegate: create a child Paperclip issue under the case's
main issue, assigned to the analyst of the correct company, and trigger
a wakeup with `mutation: extract_appraiser_facts`. The analyst runs the
MCP tool locally and posts results as a comment.
Pre-check: short-circuits with `sides_missing` if any appraisal is
untagged, so the chair gets immediate feedback without spinning up an
agent for nothing. The check uses `_validate_sides_tagged` against the
documents already in the DB — no LLM call, safe to run in-container.
Response shape:
{"status": "queued", "sub_issue_id", "analyst_id", "main_issue_id"}
or {"status": "sides_missing", "missing": [...], "message": "..."}
or {"status": "no_appraisals", ...}
or {"status": "skipped", "reason": "no_api_key"|"no_analyst"|"no_issue"}
"""
from legal_mcp.services import appraiser_facts_extractor
from legal_mcp.services import db as mcp_db
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"תיק {case_number} לא נמצא")
# Pre-validate without touching Claude — surface sides_missing directly
# so the UI can show the list of untagged appraisals immediately.
docs = await mcp_db.list_documents(UUID(case["id"]))
appraisals = [d for d in docs if d.get("doc_type") == "appraisal"]
if not appraisals:
return {
"status": "no_appraisals",
"appraisal_count": 0,
"total_facts": 0,
"conflicts": [],
}
missing = appraiser_facts_extractor._validate_sides_tagged(appraisals)
if missing:
return {
"status": "sides_missing",
"appraisal_count": len(appraisals),
"missing": missing,
"message": (
"חסר תיוג appraiser_side במסמכי שומה. תייג כל שומה דרך ה-UI "
"(ועדה / עורר / מכריע) והרץ שוב."
),
}
# Route to the analyst of the correct company by case-number prefix
prefix = case_number[:1]
company_id = (
PAPERCLIP_COMPANIES["licensing"] if prefix == "1"
else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9")
else ""
)
try:
result = await appraiser_facts_extractor.extract_appraiser_facts(
UUID(case["id"])
result = await pc_wake_analyst_for_appraiser_facts(
case_number, company_id=company_id,
)
except Exception as e:
raise HTTPException(500, f"חילוץ נכשל: {e}")
logger.exception("analyst wakeup failed for %s", case_number)
raise HTTPException(500, f"לא ניתן לשלוח לאנליטיקאי: {e}")
return result