fix(appraiser-facts): route extraction through analyst wakeup (was silent 0)
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m38s
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:
67
web/app.py
67
web/app.py
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user