feat(feedback): סימון "יושם" מפעיל CEO לקיפול הלקח לקובץ הנכון

סוגר את לולאת פידבק-יו"ר→ידע-סוכנים. עד כה resolve רק עדכן את ה-DB; עכשיו
לחיצה ב-/feedback מעירה את ה-CEO שמקפל את הלקח לקובץ לפי הקטגוריה.

- paperclip_client.py: wake_ceo_for_feedback_fold() — יוצר issue ב-Paperclip
  עם הלקח + rubric ניתוב (style→SKILL.md, wrong_structure→block-schema,
  אחר→lessons.md), מעיר CEO. משכפל את דפוס wake_for_precedent_extraction
- db.py: get_chair_feedback(id) — שליפת הערה בודדת עם case_number/appeal_type
- app.py: resolve endpoint מקבל fold (ברירת מחדל true); BackgroundTask
  fire-and-forget; guard — רק עם lesson_extracted. מחזיר fold_queued
- legal-ceo.md: dispatch ל-feedback_fold_ + סעיף "קיפול הערת יו"ר" עם rubric
- frontend: useResolveFeedback מקבל fold; /feedback שולח fold=true עם toast;
  drafts-panel שולח fold=false (bookkeeping per-case, בלי קיפול כפול)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 13:08:41 +00:00
parent dd0e754dad
commit 4174217179
7 changed files with 212 additions and 9 deletions

View File

@@ -907,6 +907,119 @@ async def wake_for_precedent_extraction(
return {"ok": False, "error": f"wakeup: {e}", "issue_id": issue_id, "identifier": identifier}
async def wake_ceo_for_feedback_fold(
*,
feedback_id: str,
feedback_text: str,
lesson_extracted: str,
category: str,
block_id: str = "",
case_number: str = "",
practice_area: str = "",
) -> dict:
"""Wake the CEO to fold one resolved chair-feedback lesson into the right
knowledge file (the feedback→agent-knowledge loop).
Mirrors ``wake_for_precedent_extraction``: creates a Paperclip issue under
the library project carrying the lesson + a category→file routing rubric,
then wakes the CEO (reason ``feedback_fold_<identifier>``). The CEO folds
the lesson per the rubric and closes the issue.
Best-effort: any failure is logged and returned as ``{"ok": False, ...}``
so resolving the feedback never fails because Paperclip is down.
"""
if not PAPERCLIP_BOARD_API_KEY:
logger.warning("PAPERCLIP_BOARD_API_KEY not set — skipping feedback-fold wakeup")
return {"ok": False, "skipped": "no_api_key"}
# decision-lessons.md is shared across companies → either CEO works; route
# by the case's practice area when known, else default to licensing.
company_id = await _get_company_id(practice_area)
ceo_id = CEO_AGENTS.get(company_id, CEO_AGENT_ID)
# category → target file routing rubric (embedded in the issue so the CEO
# has it even if its prompt is trimmed). Keep in sync with legal-ceo.md.
routing = {
"style": "skills/decision/SKILL.md",
"wrong_structure": "docs/block-schema.md + docs/legal-decision-lessons.md",
"missing_content": "docs/legal-decision-lessons.md",
"factual_error": "docs/legal-decision-lessons.md",
"wrong_tone": "docs/legal-decision-lessons.md",
"other": "שיקול דעת — לרוב באג מערכת → משימת TaskMaster, לא לקח",
}
target = routing.get(category, "docs/legal-decision-lessons.md")
try:
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
try:
project_id = await _get_or_create_library_project(conn, company_id)
row = await conn.fetchrow(
"UPDATE companies SET issue_counter = issue_counter + 1 "
"WHERE id = $1::uuid RETURNING issue_counter",
company_id,
)
issue_number = row["issue_counter"]
prefix = "CMP" if company_id == COMPANIES["licensing"] else "CMPA"
identifier = f"{prefix}-{issue_number}"
issue_id = str(uuid.uuid4())
short = (feedback_text[:90] + "") if len(feedback_text) > 90 else feedback_text
description = (
f"היו\"ר סימנה הערת פידבק כ\"יושמה\" — יש לקפל את הלקח לקובץ הידע הנכון.\n\n"
f"**feedback_id:** `{feedback_id}`\n"
f"**קטגוריה:** {category}\n"
f"**בלוק:** {block_id or ''}\n"
f"**תיק:** {case_number or ''}\n\n"
f"**ההערה:**\n{feedback_text}\n\n"
f"**הלקח שהופק:**\n{lesson_extracted or '(אין — הפק לקח מההערה)'}\n\n"
f"---\n\n"
f"**יעד קיפול לפי הקטגוריה:** `{target}`\n\n"
f"**משימה:**\n"
f"1. קרא את קובץ היעד והבן מה כבר מתועד שם.\n"
f"2. הוסף את הלקח **רק אם אינו קיים** (לא כפל). משפט אחד ברור + Rule באנגלית.\n"
f"3. אם הקטגוריה היא `other` והלקח הוא באג מערכת ולא לקח כתיבה — "
f"אל תוסיף לקובץ; פתח/עדכן משימת TaskMaster במקום.\n"
f"4. סמן את ה-issue כ-done ופתח comment קצר: לאיזה קובץ קופל ומה נוסף.\n"
)
await conn.execute(
"""INSERT INTO issues
(id, company_id, project_id, title, description,
status, priority, issue_number, identifier, assignee_agent_id)
VALUES ($1, $2::uuid, $3::uuid, $4, $5, 'todo', 'low',
$6, $7, $8::uuid)""",
issue_id, company_id, project_id,
f"[לקח] קיפול הערת יו\"ר: {short}"[:200],
description,
issue_number, identifier, ceo_id,
)
finally:
await conn.close()
except Exception as e:
logger.exception("wake_ceo_for_feedback_fold: DB step failed: %s", e)
return {"ok": False, "error": f"db: {e}"}
payload = {
"source": "automation",
"triggerDetail": "feedback_resolved",
"reason": f"feedback_fold_{identifier}",
"payload": {
"issueId": issue_id,
"mutation": "feedback_fold",
"feedbackId": feedback_id,
},
}
try:
resp = await pc_request(
"POST", f"/api/agents/{ceo_id}/wakeup",
json=payload, raise_on_error=True,
)
logger.info("feedback-fold wakeup queued: issue=%s feedback=%s", identifier, feedback_id)
return {"ok": True, "issue_id": issue_id, "identifier": identifier, "wakeup": resp.json()}
except Exception as e:
logger.exception("wake_ceo_for_feedback_fold: wakeup API failed: %s", e)
return {"ok": False, "error": f"wakeup: {e}", "issue_id": issue_id, "identifier": identifier}
async def wake_ceo_agent(issue_id: str, case_number: str, company_id: str = "") -> dict:
"""Wake the CEO agent via Paperclip's wakeup API.