feat(case-ui): כרטיס "הפקת מסמכים" עם 3 כפתורי-הפק דטרמיניסטיים (#214)
מוסיף כרטיס "הפקת מסמכים" בראש טאב "טיוטות והערות" (mockup 26 — תוספת ל-18h),
תוספת לעמוד הקיים, כל הסקשנים נשמרים.
- כפתור 1 "הפק סיכום מנהלים" → endpoint חדש POST /generate/party-claims-summary
→ CEO wakeup סטרוקטורלי action="party_claims_summary" (שלב H2, host-side).
Polling: GET /research/party-claims-summary (200=מוכן → פתח/DOCX).
- כפתור 2 "הפק טיוטה של טענות הצדדים" → endpoint חדש POST /generate/interim-draft
→ CEO wakeup action="interim_draft" (שלב H, host-side).
Polling: רשימת ה-exports עד שמופיע טיוטת-ביניים-{case}-vN.docx.
- כפתור 3 "הפק טיוטת החלטה מלאה" → ה-useExportDocx הקיים (in-container, ללא LLM);
הועבר מהבאנר (כפתור "הפק DOCX" הוסר משם — אין כפילות). disabled+title כשאין
בלוקים כתובים (isDraftReady).
מסלול-עירור יחיד דרך ה-Platform Port (pc_wake_ceo_for_action) → API helper
/api/agents/{id}/wakeup, לעולם לא DB insert. Israel-time + cache-invalidation.
Invariants: G12 (מגע-Paperclip רק ב-paperclip_client + agent_platform_port;
leak-guard ירוק) · G2 (אין מסלול-עירור מקביל — שימוש חוזר ב-pc_request הקיים) ·
G10 (הפקה מגודרת-יו"ר/CEO) · INV-UI9 (Asia/Jerusalem) · X6 (UI↔API contract).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,7 @@ from web.paperclip_client import (
|
||||
wake_analyst_for_appraiser_facts as pc_wake_analyst_for_appraiser_facts,
|
||||
wake_analyst_for_argument_aggregation as pc_wake_analyst_for_argument_aggregation,
|
||||
wake_ceo_agent as pc_wake_ceo,
|
||||
wake_ceo_for_action as pc_wake_ceo_for_action,
|
||||
wake_ceo_for_feedback_fold as pc_wake_ceo_for_feedback_fold,
|
||||
wake_curator_for_final as pc_wake_curator_for_final,
|
||||
wake_for_precedent_extraction as pc_wake_for_precedent_extraction,
|
||||
@@ -97,6 +98,7 @@ __all__ = [
|
||||
"pc_get_agents_for_case",
|
||||
"pc_get_agents",
|
||||
"pc_wake_ceo",
|
||||
"pc_wake_ceo_for_action",
|
||||
"pc_wake_ceo_for_feedback_fold",
|
||||
"pc_wake_curator_for_final",
|
||||
"pc_wake_for_precedent_extraction",
|
||||
|
||||
46
web/app.py
46
web/app.py
@@ -79,6 +79,7 @@ from web.agent_platform_port import (
|
||||
pc_wake_analyst_for_argument_aggregation,
|
||||
rename_case_project,
|
||||
pc_wake_ceo,
|
||||
pc_wake_ceo_for_action,
|
||||
pc_wake_ceo_for_feedback_fold,
|
||||
pc_wake_curator_for_final,
|
||||
pc_wake_for_precedent_extraction,
|
||||
@@ -3090,6 +3091,51 @@ async def api_party_claims_summary(case_number: str):
|
||||
return {"markdown": path.read_text(encoding="utf-8")}
|
||||
|
||||
|
||||
# ── Deterministic document generation triggers (TaskMaster #214) ─────
|
||||
# The "הפקת מסמכים" buttons in the case "טיוטות והערות" tab. Generation for the
|
||||
# executive summary + interim draft runs HOST-SIDE (claude_session → claude -p,
|
||||
# not in the container), so these endpoints fire a *structured-action* CEO wakeup
|
||||
# (the CEO reads payload.action and routes to שלב H2 / שלב H — legal-ceo.md).
|
||||
# Wakeup goes through the Paperclip API helper (pc_wake_ceo_for_action → the
|
||||
# platform Port → POST /api/agents/{id}/wakeup), NEVER a direct DB insert.
|
||||
# Polling for completion uses the existing read endpoints (research/party-claims-
|
||||
# summary for the summary; the exports list for the טיוטת-ביניים-*.docx file).
|
||||
|
||||
|
||||
async def _wake_ceo_action(case_number: str, action: str) -> dict:
|
||||
"""Resolve the case's company + fire a structured-action CEO wakeup (#214)."""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
raise HTTPException(404, f"תיק {case_number} לא נמצא")
|
||||
prefix = case_number[:1]
|
||||
company_id = (
|
||||
PAPERCLIP_COMPANIES["licensing"] if prefix == "1"
|
||||
else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9")
|
||||
else ""
|
||||
)
|
||||
try:
|
||||
return await pc_wake_ceo_for_action(case_number, action, company_id=company_id)
|
||||
except Exception as e:
|
||||
logger.warning("CEO action %s wakeup failed for %s: %s", action, case_number, e)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/generate/party-claims-summary", status_code=202)
|
||||
async def api_generate_party_claims_summary(case_number: str):
|
||||
"""Fire-and-accept: wake the CEO to generate the party-claims executive summary
|
||||
(שלב H2 → summarize_party_claims). Runs host-side; poll
|
||||
GET /api/cases/{n}/research/party-claims-summary for completion."""
|
||||
return await _wake_ceo_action(case_number, "party_claims_summary")
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/generate/interim-draft", status_code=202)
|
||||
async def api_generate_interim_draft(case_number: str):
|
||||
"""Fire-and-accept: wake the CEO to generate the partial "party-claims" draft
|
||||
(שלב H → write_interim_draft + export_interim_draft). Runs host-side; poll the
|
||||
exports list for the new טיוטת-ביניים-{case}-vN.docx."""
|
||||
return await _wake_ceo_action(case_number, "interim_draft")
|
||||
|
||||
|
||||
@app.get("/api/cases/{case_number}/research/party-claims-summary/download")
|
||||
async def api_party_claims_summary_download(case_number: str):
|
||||
"""Download the raw party-claims-summary.md file."""
|
||||
|
||||
@@ -1195,6 +1195,73 @@ async def wake_ceo_agent(issue_id: str, case_number: str, company_id: str = "")
|
||||
return result
|
||||
|
||||
|
||||
async def wake_ceo_for_action(
|
||||
case_number: str,
|
||||
action: str,
|
||||
company_id: str = "",
|
||||
) -> dict:
|
||||
"""Wake the CEO with a deterministic *structured action* on the case's MAIN issue.
|
||||
|
||||
Drives the UI "הפקת מסמכים" buttons (TaskMaster #214): the CEO reads
|
||||
``payload.action`` and routes deterministically to its שלב H / שלב H2
|
||||
side-quests (legal-ceo.md §"פעולות סטרוקטורליות") — no free-text parsing.
|
||||
|
||||
action="party_claims_summary" → שלב H2 (summarize_party_claims)
|
||||
action="interim_draft" → שלב H (write_interim_draft + export_interim_draft)
|
||||
|
||||
These are side-quests on the EXISTING ``[ערר {case_number}]`` issue — no child
|
||||
issue is created (the CEO must not change ``cases.status`` or spawn sub-agents).
|
||||
Wakeup goes through the Paperclip API (``POST /api/agents/{id}/wakeup``), never a
|
||||
direct DB insert (CLAUDE.md hard rule). Returns
|
||||
``{"status": "ok"|"skipped", ...}``.
|
||||
"""
|
||||
if not PAPERCLIP_BOARD_API_KEY:
|
||||
logger.warning("PAPERCLIP_BOARD_API_KEY not set — skipping CEO action wakeup")
|
||||
return {"status": "skipped", "reason": "no_api_key"}
|
||||
|
||||
issues = await get_case_issues(case_number)
|
||||
if not issues:
|
||||
logger.warning("No Paperclip issues found for case %s — skipping CEO action", case_number)
|
||||
return {"status": "skipped", "reason": "no_issue"}
|
||||
|
||||
# The main case issue — prefer the in-progress one, else the canonical
|
||||
# "[ערר {case_number}]" issue, else the oldest. (Same selection spirit as
|
||||
# wake_curator_for_final, but we never spawn a child — it's a side-quest.)
|
||||
main_issue = (
|
||||
next((i for i in issues if i.get("status") == "in_progress"), None)
|
||||
or next((i for i in issues if f"[ערר {case_number}]" in (i.get("title") or "")), None)
|
||||
or issues[0]
|
||||
)
|
||||
main_issue_id = main_issue["id"]
|
||||
|
||||
ceo_id = CEO_AGENTS.get(company_id, CEO_AGENT_ID)
|
||||
wake_resp = await pc_request(
|
||||
"POST",
|
||||
f"/api/agents/{ceo_id}/wakeup",
|
||||
json={
|
||||
"source": "on_demand",
|
||||
"triggerDetail": "manual",
|
||||
"reason": f"generate_{action}_{case_number}",
|
||||
"payload": {
|
||||
"issueId": main_issue_id,
|
||||
"action": action,
|
||||
"case_number": case_number,
|
||||
},
|
||||
},
|
||||
raise_on_error=True,
|
||||
)
|
||||
logger.info(
|
||||
"CEO action wakeup for case %s: action=%s issue=%s ceo=%s status=%s",
|
||||
case_number, action, main_issue_id, ceo_id, wake_resp.status_code,
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"action": action,
|
||||
"issue_id": main_issue_id,
|
||||
"ceo_id": ceo_id,
|
||||
}
|
||||
|
||||
|
||||
def _curator_task_brief(task: str, case_number: str, final_filename: str) -> tuple[str, str]:
|
||||
"""Build the (sub-issue title, description) for a staged final-decision task.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user