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:
@@ -53,6 +53,15 @@ CURATOR_AGENTS = {
|
||||
COMPANIES["betterment"]: "d6f7c55d-570a-46b8-8d72-1286d07da0d8", # CMPA curator
|
||||
}
|
||||
|
||||
# Legal Analyst (מנתח משפטי) agent per company — woken from the chair UI
|
||||
# when the chair finishes tagging appraisals and asks for fact extraction.
|
||||
# The analyst runs `mcp__legal-ai__extract_appraiser_facts` locally (where
|
||||
# the Claude CLI is present), since the FastAPI container cannot.
|
||||
ANALYST_AGENTS = {
|
||||
COMPANIES["licensing"]: "c26e9439-a88a-49dc-9e67-2262c95db65c", # CMP analyst
|
||||
COMPANIES["betterment"]: "f70fd353-6cde-46b3-8d6c-cfad12100b1b", # CMPA analyst
|
||||
}
|
||||
|
||||
# Fallback mapping — used only when DB lookup returns no results.
|
||||
# בל"מ (extension_request_*) variants route to the same company as their
|
||||
# parent domain — בל"מ ברישוי → CMP, בל"מ בהיטל השבחה → CMPA, וכו'.
|
||||
@@ -1016,3 +1025,107 @@ async def wake_curator_for_final(
|
||||
"curator_id": curator_id,
|
||||
"main_issue_id": main_issue_id,
|
||||
}
|
||||
|
||||
|
||||
async def wake_analyst_for_appraiser_facts(
|
||||
case_number: str,
|
||||
company_id: str,
|
||||
) -> dict:
|
||||
"""Wake the legal-analyst to extract appraiser facts for this case.
|
||||
|
||||
Triggered by the chair clicking "חלץ עובדות שמאיות עכשיו" in the UI.
|
||||
The FastAPI container cannot run `extract_appraiser_facts` directly —
|
||||
the extractor calls `claude_session.query_json()`, which only works
|
||||
where the local `claude` CLI is present (the MCP server / agent runner
|
||||
on the host). So instead of running it inline, we create a child issue
|
||||
under the case's main Paperclip issue, assign it to the analyst of the
|
||||
correct company, and trigger a wakeup with `mutation: extract_appraiser_facts`.
|
||||
The analyst's HEARTBEAT picks up the issue, runs the MCP tool locally,
|
||||
and reports back via a comment.
|
||||
|
||||
Returns a dict shaped for the FastAPI endpoint to serialize as-is:
|
||||
{"status": "queued", "sub_issue_id", "analyst_id", "main_issue_id"}
|
||||
or {"status": "skipped", "reason": "..."} for non-fatal early outs.
|
||||
"""
|
||||
if not PAPERCLIP_BOARD_API_KEY:
|
||||
logger.warning(
|
||||
"PAPERCLIP_BOARD_API_KEY not set — cannot queue analyst wakeup for %s",
|
||||
case_number,
|
||||
)
|
||||
return {"status": "skipped", "reason": "no_api_key"}
|
||||
|
||||
analyst_id = ANALYST_AGENTS.get(company_id)
|
||||
if not analyst_id:
|
||||
logger.info("No analyst configured for company %s — skipping", company_id)
|
||||
return {"status": "skipped", "reason": "no_analyst", "company_id": company_id}
|
||||
|
||||
issues = await get_case_issues(case_number)
|
||||
if not issues:
|
||||
logger.warning(
|
||||
"No Paperclip issues found for case %s — cannot queue analyst", case_number,
|
||||
)
|
||||
return {"status": "skipped", "reason": "no_issue"}
|
||||
|
||||
main_issue = next((i for i in issues if i.get("status") == "in_progress"), None) or issues[0]
|
||||
main_issue_id = main_issue["id"]
|
||||
|
||||
description = (
|
||||
f"חיים תייג שומות בתיק {case_number} וביקש חילוץ עובדות שמאיות.\n\n"
|
||||
f"הרץ `mcp__legal-ai__extract_appraiser_facts(case_number=\"{case_number}\")` "
|
||||
f"וכתוב comment בעברית עם תוצאת החילוץ — מספר תכניות, מספר היתרים, "
|
||||
f"וסתירות (אם יש) בין שמאים. אם המסמכים חסרי תיוג `appraiser_side`, "
|
||||
f"דווח ב-comment על השומות החסרות וסגור את ה-issue כ-blocked."
|
||||
)
|
||||
child_resp = await pc_request(
|
||||
"POST",
|
||||
f"/api/issues/{main_issue_id}/children",
|
||||
json={
|
||||
"title": f"[ערר {case_number}] חילוץ עובדות שמאיות",
|
||||
"description": description,
|
||||
"status": "in_progress",
|
||||
"priority": "normal",
|
||||
"assigneeAgentId": analyst_id,
|
||||
},
|
||||
raise_on_error=True,
|
||||
)
|
||||
sub_issue = child_resp.json()
|
||||
sub_issue_id = sub_issue["id"]
|
||||
|
||||
# Tag plugin_state so the case page surfaces this sub-issue too
|
||||
try:
|
||||
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
||||
try:
|
||||
await _link_case_to_issue(conn, sub_issue_id, case_number)
|
||||
finally:
|
||||
await conn.close()
|
||||
except Exception as e:
|
||||
logger.warning("plugin_state link failed for sub_issue=%s: %s", sub_issue_id, e)
|
||||
|
||||
wake_resp = await pc_request(
|
||||
"POST",
|
||||
f"/api/agents/{analyst_id}/wakeup",
|
||||
json={
|
||||
"source": "on_demand",
|
||||
"triggerDetail": "manual",
|
||||
"reason": f"extract_appraiser_facts_{case_number}",
|
||||
# Use "assignment" — the same mutation `wake_curator_for_final`
|
||||
# sends. The HEARTBEAT recognises it; the task-specific intent
|
||||
# is conveyed by the child-issue's description, not the payload.
|
||||
"payload": {
|
||||
"issueId": sub_issue_id,
|
||||
"mutation": "assignment",
|
||||
"caseNumber": case_number,
|
||||
},
|
||||
},
|
||||
raise_on_error=True,
|
||||
)
|
||||
logger.info(
|
||||
"Analyst wakeup for case %s: sub_issue=%s analyst=%s wake=%s",
|
||||
case_number, sub_issue_id, analyst_id, wake_resp.status_code,
|
||||
)
|
||||
return {
|
||||
"status": "queued",
|
||||
"sub_issue_id": sub_issue_id,
|
||||
"analyst_id": analyst_id,
|
||||
"main_issue_id": main_issue_id,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user