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
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:
@@ -19,9 +19,15 @@ profiles:
|
||||
- **CMP** (תיקים 1xxx): רישוי ובניה. profile=`curator-cmp`. UUID `60dce831-...`
|
||||
- **CMPA** (תיקים 8xxx + 9xxx): היטלי השבחה ופיצויים. profile=`curator-cmpa`. UUID `d6f7c55d-...`
|
||||
|
||||
ה-CEO (`עוזר משפטי`, `claude_local`) הוא ה-orchestrator הראשי בכל חברה.
|
||||
אני סוכן-משנה שמופעל אחרי שלב F (ייצוא DOCX) של ה-CEO. אני לא מחליף
|
||||
אף סוכן קיים — מוסיף שכבת ניתוח חדשה.
|
||||
**איך אני מופעל:** דפנה לוחצת "סמן כסופי" בקובץ ב-UI של legal-ai →
|
||||
`POST /api/cases/{case_number}/exports/{filename}/mark-final` רץ ב-`web/app.py` →
|
||||
הוא קורא ל-`pc_wake_curator_for_final()` ב-`web/paperclip_client.py` שיוצר
|
||||
לי sub-issue ומעיר אותי. **לא דרך CEO** — חיבור ישיר מהאירוע ב-UI לסוכן.
|
||||
זה מבטיח שאני מנתח את הגרסה האמיתית של דפנה, לא טיוטה אינטרמדיאטית.
|
||||
|
||||
ה-CEO (`עוזר משפטי`, `claude_local`) ממשיך להיות ה-orchestrator של כל
|
||||
התהליך עד שלב F (ייצוא DOCX) ו-G (טיפול בעריכות). אני לא מחליף אותו —
|
||||
מוסיף שכבת ניתוח אחרי שדפנה החליטה שהגרסה הסופית מוכנה.
|
||||
|
||||
## תפקיד
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ tools:
|
||||
| כותב החלטה | 7ed8686f-24bc-49a3-bc02-67ca15b895a9 | כתיבת בלוקים ה-יב (Opus) |
|
||||
| בודק איכות | 1a5b229e-9220-4b13-940c-f8eb7285fc29 | QA לפני ייצוא |
|
||||
| מייצא טיוטה | d0dc703b-ca83-4883-bca7-c9449e8713cd | בדיקה סופית + ייצוא DOCX מגורסת |
|
||||
| מנהל ידע (Hermes) | CMP: 60dce831-5c5b-4bae-bda9-5282d506f0dc · CMPA: d6f7c55d-570a-46b8-8d72-1286d07da0d8 | סקירת החלטות סופיות, הצעות לעדכון style guide / lessons (POC — Hermes Agent). |
|
||||
| מנהל ידע (Hermes) | CMP: 60dce831-5c5b-4bae-bda9-5282d506f0dc · CMPA: d6f7c55d-570a-46b8-8d72-1286d07da0d8 | סקירת החלטות סופיות, הצעות לעדכון style guide / lessons. **לא קורא ישירות מ-CEO** — מופעל אוטומטית מ-`web/app.py:api_mark_final` כשדפנה לוחצת "סמן כסופי" ב-UI. |
|
||||
|
||||
## כלל: כל issue חדש = תת-משימה
|
||||
|
||||
@@ -452,61 +452,6 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
|
||||
|
||||
**מתי לחזור אחורה:** אם דוח QA מצביע על בעיה מתודולוגית (סילוגיזם חסר, כיוון לא תואם chair_directions) — חזור לשלב C/D ולא רק לכותב.
|
||||
|
||||
### שלב F2: סקירת ידע (Knowledge Curator)
|
||||
|
||||
**מתי:** רק אחרי ש-F הושלם בהצלחה (ייצוא הצליח, comment "החלטה מוכנה לביקורת" פורסם).
|
||||
|
||||
**מטרה:** להפעיל את **מנהל הידע** (Hermes) שיסקור את ההחלטה הסופית ויציע
|
||||
עדכוני style guide / lessons. read-only על תוכן, write רק על comments.
|
||||
בלאסט-רדיוס אפס — אם נכשל לא משנה את זרימת F.
|
||||
|
||||
**זמין בשתי החברות (CMP + CMPA)** — כל אחת עם profile והחלטות-מקור משלה.
|
||||
ה-lookup לפי שם מוצא את ה-curator הנכון בחברה הנוכחית.
|
||||
|
||||
**תהליך:**
|
||||
|
||||
```bash
|
||||
# 1. בדוק אם קיים curator בחברה שלך
|
||||
CURATOR_ID=$(PGPASSWORD=paperclip psql -h localhost -p 54329 -U paperclip -d paperclip -tA -c \
|
||||
"SELECT id FROM agents WHERE name='מנהל ידע' AND company_id='$PAPERCLIP_COMPANY_ID' LIMIT 1;")
|
||||
if [ -z "$CURATOR_ID" ]; then
|
||||
echo "[F2] No Knowledge Curator in company — skipping"
|
||||
else
|
||||
# 2. מצא את ה-issue הראשי של התיק
|
||||
MAIN_ISSUE_ID=$(curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
"$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('parentId') or d['id'])")
|
||||
|
||||
# 3. צור sub-issue ל-curator (ללא המתנה לתשובה!)
|
||||
SUB_ISSUE_ID=$(~/legal-ai/scripts/pc.sh POST "/api/issues/$MAIN_ISSUE_ID/children" "$(cat <<JSON
|
||||
{
|
||||
"title": "[ערר $CASE_NUMBER] סקירת ידע — Knowledge Curator",
|
||||
"description": "סקור את ההחלטה הסופית שיוצאה כעת. חפש 3-5 דפוסי סגנון/דיון שלא תועדו ב-skills/decision/SKILL.md או ב-docs/legal-decision-lessons.md. כתוב comment עם הממצאים בעברית, ניטרלי. אל תעדכן קבצים — רק הצע.",
|
||||
"status": "in_progress",
|
||||
"priority": "low",
|
||||
"assigneeAgentId": "$CURATOR_ID"
|
||||
}
|
||||
JSON
|
||||
)" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||
|
||||
# 4. תייג plugin_state (חובה לפי כלל ברזל בראש הקובץ)
|
||||
PGPASSWORD=paperclip psql -h localhost -p 54329 -U paperclip -d paperclip -c \
|
||||
"INSERT INTO plugin_state (plugin_id, scope_kind, scope_id, namespace, state_key, value_json)
|
||||
VALUES ('53461b5a-7f58-411a-9952-72f9c8d4a328', 'issue', '$SUB_ISSUE_ID', 'default', 'legal-case-number', '\"$CASE_NUMBER\"')
|
||||
ON CONFLICT DO NOTHING;"
|
||||
|
||||
# 5. דווח בקומנט הראשי (לא ממתין לתוצאה)
|
||||
~/legal-ai/scripts/pc.sh POST "/api/issues/$PAPERCLIP_TASK_ID/comments" \
|
||||
"$(printf '{"body":"F2 ✓ — מנהל הידע (Hermes) מנתח את ההחלטה אסינכרונית. הממצאים יופיעו ב-CMP-XX (sub-issue %s)."}' "$SUB_ISSUE_ID")"
|
||||
fi
|
||||
```
|
||||
|
||||
**חשוב:**
|
||||
- **אל תמתין** לתשובת ה-curator — F הסתיים, לא חוסם את זרימה.
|
||||
- **אל תעצור** את F בגלל F2. אם יצירת sub-issue נכשלה — log warning ועבור הלאה.
|
||||
- ה-curator הוא Hermes (לא Claude Code) — POC. אם אתה רואה התנהגות מוזרה ב-sub-issues שלו, זה צפוי כרגע.
|
||||
- ה-curator לא סוגר את ה-issue אוטומטית עדיין — צפוי. תיקון בעתיד (followup).
|
||||
|
||||
### שלב G: טיפול בעריכה מהמשתמש (אחרי ייצוא)
|
||||
|
||||
**מתי:** המשתמש העלה `עריכה-v*.docx` (אחרי שייצאנו `טיוטה-v*.docx` קודמת) וכתב תגובה בקומנט.
|
||||
|
||||
34
web/app.py
34
web/app.py
@@ -62,6 +62,7 @@ from web.paperclip_client import (
|
||||
respond_to_interaction as pc_respond_to_interaction,
|
||||
restore_project as pc_restore_project,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -2347,10 +2348,43 @@ async def api_mark_final(case_number: str, filename: str):
|
||||
if case_dir.exists():
|
||||
commit_and_push(case_dir, f"גרסה סופית: {final_name}")
|
||||
|
||||
# Best-effort: ingest final into internal corpus (case_law) — feeds
|
||||
# search_decisions and learning_loop. Non-fatal on failure.
|
||||
ingest_status: dict = {"status": "skipped"}
|
||||
try:
|
||||
ingest_result = await workflow_tools.ingest_final_version(
|
||||
case_number, file_path=str(final_path)
|
||||
)
|
||||
ingest_status = {"status": "ok", "result": (ingest_result or "")[:200]}
|
||||
except Exception as e:
|
||||
logger.warning("ingest_final_version failed for %s: %s", case_number, e)
|
||||
ingest_status = {"status": "error", "error": str(e)}
|
||||
|
||||
# Best-effort: wake the Knowledge Curator (Hermes) to analyze the
|
||||
# signed final and propose updates to skills/lessons. Non-fatal on
|
||||
# failure so marking final never breaks for the user.
|
||||
curator_status: dict = {"status": "skipped"}
|
||||
try:
|
||||
# Company by case-number prefix: 1xxx=CMP (licensing), 8/9xxx=CMPA (betterment)
|
||||
prefix = case_number[:1]
|
||||
company_id = (
|
||||
PAPERCLIP_COMPANIES["licensing"] if prefix == "1"
|
||||
else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9")
|
||||
else ""
|
||||
)
|
||||
curator_status = await pc_wake_curator_for_final(
|
||||
case_number, final_name, company_id=company_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("curator wakeup failed for %s: %s", case_number, e)
|
||||
curator_status = {"status": "error", "error": str(e)}
|
||||
|
||||
return {
|
||||
"final_filename": final_name,
|
||||
"training_copy": str(training_dest),
|
||||
"status": "final",
|
||||
"ingest_final": ingest_status,
|
||||
"curator": curator_status,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user