feat(curator): trigger Knowledge Curator from api_mark_final, drop CEO F2
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s

The previous F2 stage in legal-ceo.md fired after the first DOCX export
— too early, since the user often iterates with עריכה-* uploads after
the first export. The true "this is dafna's chosen final" signal is the
"סמן כסופי" button in the UI, which calls api_mark_final.

This commit moves the curator wakeup from CEO's instructions to a
direct hook in api_mark_final:

- web/paperclip_client.py: add CURATOR_AGENTS dict (CMP + CMPA UUIDs)
  and wake_curator_for_final() helper. Looks up main case issue,
  creates a child issue assigned to the curator, tags plugin_state for
  case visibility, and triggers wakeup via Paperclip API.
- web/app.py: api_mark_final now calls workflow_tools.ingest_final_version
  (so case_law table finally gets populated for search_decisions) and
  pc_wake_curator_for_final. Both are best-effort — failure does not
  block marking final.
- legal-ceo.md: remove F2 stage, leave only the agents-table reference
  noting the curator runs from api_mark_final.
- hermes-curator.md: update activation description to reflect the new
  flow.

Result: curator runs only when chaim deliberately clicks "סמן כסופי",
on the actual final file, with no risk of analyzing a draft that will
later change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 14:47:03 +00:00
parent 77e5996497
commit 799b950961
4 changed files with 141 additions and 59 deletions

View File

@@ -45,6 +45,14 @@ CEO_AGENTS = {
# Default for backwards compat
CEO_AGENT_ID = CEO_AGENTS[COMPANIES["licensing"]]
# Knowledge Curator (Hermes) agent per company — woken after a case is
# marked final, to analyze the signed decision and propose updates to
# the style guide / lessons. POC stage 1.
CURATOR_AGENTS = {
COMPANIES["licensing"]: "60dce831-5c5b-4bae-bda9-5282d506f0dc", # CMP curator
COMPANIES["betterment"]: "d6f7c55d-570a-46b8-8d72-1286d07da0d8", # CMPA curator
}
# Fallback mapping — used only when DB lookup returns no results
_FALLBACK_APPEAL_TYPE_TO_COMPANY = {
"רישוי": COMPANIES["licensing"],
@@ -898,3 +906,92 @@ async def wake_ceo_agent(issue_id: str, case_number: str, company_id: str = "")
result = resp.json()
logger.info("CEO agent wakeup for case %s: %s", case_number, result)
return result
async def wake_curator_for_final(
case_number: str,
final_filename: str,
company_id: str = "",
) -> dict:
"""Wake the Knowledge Curator (Hermes) when a case is marked final.
Creates a child issue under the main case issue, assigns it to the
curator, and triggers wakeup. Best-effort — silently skips if no
curator is configured for the company or no main issue is found.
Returns ``{"status": "ok"|"skipped", ...}``.
"""
if not PAPERCLIP_BOARD_API_KEY:
logger.warning("PAPERCLIP_BOARD_API_KEY not set — skipping curator wakeup")
return {"status": "skipped", "reason": "no_api_key"}
curator_id = CURATOR_AGENTS.get(company_id)
if not curator_id:
logger.info("No curator configured for company %s — skipping", company_id)
return {"status": "skipped", "reason": "no_curator", "company_id": company_id}
issues = await get_case_issues(case_number)
if not issues:
logger.warning("No Paperclip issues found for case %s — skipping curator", 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"
f"קובץ סופי: `{final_filename}`\n\n"
f"סקור את ההחלטה מול skills/decision/SKILL.md ו-docs/legal-decision-lessons.md.\n"
f"חפש 3-5 דפוסי סגנון/דיון שלא תועדו. כתוב comment בעברית, ניטרלי, "
f"ממוספר. עדכן את MEMORY.md שלך. סגור את ה-issue (status=done)."
)
child_resp = await pc_request(
"POST",
f"/api/issues/{main_issue_id}/children",
json={
"title": f"[ערר {case_number}] סקירת ידע — Knowledge Curator",
"description": description,
"status": "in_progress",
"priority": "low",
"assigneeAgentId": curator_id,
},
raise_on_error=True,
)
sub_issue = child_resp.json()
sub_issue_id = sub_issue["id"]
# Tag plugin_state for case-number visibility on the case page
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)
# Trigger wakeup (use API per Paperclip rule — never DB insert)
wake_resp = await pc_request(
"POST",
f"/api/agents/{curator_id}/wakeup",
json={
"source": "on_demand",
"triggerDetail": "manual",
"reason": f"final_marked_{case_number}",
"payload": {
"issueId": sub_issue_id,
"mutation": "assignment",
},
},
raise_on_error=True,
)
logger.info(
"Curator wakeup for case %s: sub_issue=%s curator=%s wake=%s",
case_number, sub_issue_id, curator_id, wake_resp.status_code,
)
return {
"status": "ok",
"sub_issue_id": sub_issue_id,
"curator_id": curator_id,
"main_issue_id": main_issue_id,
}