fix(arguments): route "חשב טיעונים" through the legal-analyst agent
The /aggregate-arguments endpoint ran an in-container BackgroundTask that called claude_session (the local `claude` CLI) — which does not exist in the FastAPI container. The button silently produced nothing, and on `force` it destructively DELETEd existing arguments *before* the doomed LLM call. Replace the inline task with the established delegation pattern used by "חלץ עובדות שמאיות" (extract-appraiser-facts): a cheap in-container DB pre-check (no_claims / exists), then a Paperclip wakeup of the company's legal-analyst, which runs mcp__legal-ai__aggregate_claims_to_arguments locally (where the CLI lives) and reports back. `force` now runs locally too, so delete+recompute are atomic on the host — no more destructive failure. Frontend: AggregateArgumentsResult becomes a discriminated union (queued | no_claims | exists | skipped) and the toast is status-accurate instead of the misleading fixed "refresh in a minute". Invariants: G12 (Paperclip touch confined to paperclip_client behind the agent_platform_port), G2 (replaces the broken path, no parallel capability), engineering §6 (explicit statuses, no silent swallow). UI change is logic/toast only (no visual-layout change) — within the Claude-Design-gate bug-fix exemption. Richer inline status panels deferred to the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1371,3 +1371,121 @@ async def wake_analyst_for_appraiser_facts(
|
||||
"analyst_id": analyst_id,
|
||||
"main_issue_id": main_issue_id,
|
||||
}
|
||||
|
||||
|
||||
async def wake_analyst_for_argument_aggregation(
|
||||
case_number: str,
|
||||
company_id: str,
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
"""Wake the legal-analyst to aggregate raw claims into legal arguments.
|
||||
|
||||
Triggered by the chair clicking "חשב טיעונים" in the case page. The
|
||||
FastAPI container cannot run `aggregate_claims_to_arguments` directly —
|
||||
the aggregator calls `claude_session.query_json()`, which only works
|
||||
where the local `claude` CLI is present (the MCP server / agent runner
|
||||
on the host), **not in this container**. So instead of an in-container
|
||||
BackgroundTask (which fails silently and, on `force`, destructively
|
||||
deletes the existing arguments before the doomed LLM call), 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. The analyst's
|
||||
HEARTBEAT picks up the issue, runs the MCP tool locally, and reports
|
||||
back via a comment.
|
||||
|
||||
Mirrors ``wake_analyst_for_appraiser_facts`` — same delegation shape.
|
||||
|
||||
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 argument aggregation on %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"]
|
||||
|
||||
force_clause = ", force=True" if force else ""
|
||||
rerun_note = (
|
||||
"זהו חישוב-מחדש (force) — הטיעונים הקיימים יימחקו ויחושבו מחדש.\n\n"
|
||||
if force else ""
|
||||
)
|
||||
description = (
|
||||
f"חיים ביקש חישוב טיעונים משפטיים בתיק {case_number}.\n\n"
|
||||
f"{rerun_note}"
|
||||
f"הרץ `mcp__legal-ai__aggregate_claims_to_arguments(case_number=\"{case_number}\"{force_clause})` "
|
||||
f"וכתוב comment בעברית עם תוצאת האיגוד — כמה טיעונים מובחנים נוצרו לכל צד "
|
||||
f"(עוררים / משיבים / ועדה / מבקשי-היתר). אם אין טענות גולמיות בתיק, דווח "
|
||||
f"ב-comment שצריך להריץ קודם חילוץ טענות (`extract_claims`) וסגור את ה-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",
|
||||
# Paperclip ISSUE_PRIORITIES = critical|high|medium|low — "normal"
|
||||
# is NOT a valid enum value (Zod 400 → surfaces as 500).
|
||||
"priority": "medium",
|
||||
"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"aggregate_arguments_{case_number}",
|
||||
# "assignment" is the generic mutation the HEARTBEAT recognises;
|
||||
# task intent lives in the child-issue description, not the payload.
|
||||
"payload": {
|
||||
"issueId": sub_issue_id,
|
||||
"mutation": "assignment",
|
||||
"caseNumber": case_number,
|
||||
},
|
||||
},
|
||||
raise_on_error=True,
|
||||
)
|
||||
logger.info(
|
||||
"Analyst wakeup for argument aggregation on case %s: sub_issue=%s "
|
||||
"analyst=%s force=%s wake=%s",
|
||||
case_number, sub_issue_id, analyst_id, force, 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