From 923903217cbcc73b2891bbaa9b0754f5c260720e Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 3 May 2026 16:49:25 +0000 Subject: [PATCH] feat(precedents): auto-trigger Claude extraction via Paperclip wakeup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a precedent is uploaded to the library, the FastAPI container now fires a Paperclip wakeup so Claude (running locally as the CEO agent) picks up the new row and runs `precedent_process_pending` for both metadata and halacha extraction. The user no longer has to remember to trigger it manually. Mechanics: - New `wake_for_precedent_extraction()` in paperclip_client.py creates (or reuses) a per-company "ספריית פסיקה — תור חילוץ" project, opens a fresh issue assigned to the company CEO with the case_law_id + citation in the description, and pings the Board API wakeup endpoint with `triggerDetail=precedent_library_upload`. - ingest_precedent's _run() in app.py captures the returned case_law_id and best-effort calls the wake function (failures are logged, not surfaced — the upload itself stays clean). - legal-ceo.md adds the precedent_process_pending tool family and a new "חילוץ פסיקה אוטומטי" section that tells the CEO to short-circuit past the heartbeat scan when woken with this trigger. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/agents/legal-ceo.md | 23 ++++++ web/app.py | 16 +++- web/paperclip_client.py | 145 ++++++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) diff --git a/.claude/agents/legal-ceo.md b/.claude/agents/legal-ceo.md index ec847ae..6ca95a6 100644 --- a/.claude/agents/legal-ceo.md +++ b/.claude/agents/legal-ceo.md @@ -29,6 +29,11 @@ tools: - mcp__legal-ai__apply_user_edit - mcp__legal-ai__list_bookmarks - mcp__legal-ai__revise_draft + - mcp__legal-ai__precedent_process_pending + - mcp__legal-ai__precedent_extract_halachot + - mcp__legal-ai__precedent_extract_metadata + - mcp__legal-ai__precedent_library_get + - mcp__legal-ai__precedent_library_list --- # עוזר משפטי — מנהל תהליך כתיבת החלטות @@ -152,8 +157,26 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru **לפני כל דבר אחר** — בדוק את סיבת ההתעוררות (`$PAPERCLIP_WAKE_REASON`): - אם ה-reason מכיל `user_commented` → **דלג ישירות לסעיף "טיפול בתגובות חדשות מחיים"**. אל תסרוק תיקים אחרים, אל תבדוק issues, אל תעשה heartbeat רגיל. **טפל רק בתגובה.** - אם ה-reason מכיל `agent_completion` → דלג לשלב E/F בהתאם לסוכן שסיים +- אם ה-reason מכיל `precedent_extraction_` → **דלג לסעיף "חילוץ פסיקה אוטומטי"**. אל תיגע בתיקים — זו עבודת ספרייה. - אחרת → המשך לשלב A (heartbeat רגיל) +### חילוץ פסיקה אוטומטי + +מופעל כשפסק דין חדש מועלה לספרייה. ה-issue נמצא בפרויקט "ספריית פסיקה — תור חילוץ" ומשויך אליך. + +**מה לעשות:** +1. קרא את ה-description של ה-issue — מצוין שם `case_law_id` וה-citation. +2. הרץ פעמיים: + ``` + mcp__legal-ai__precedent_process_pending(kind="metadata") + mcp__legal-ai__precedent_process_pending(kind="halacha") + ``` + הכלי מעבד את **כל** הפסיקות שבתור — אם תוקיע אחת והגיעו עוד בינתיים, גם הן יעובדו. +3. כשמסתיים: כתוב comment קצר ב-issue (`mcp__legal-ai__precedent_process_pending` מחזיר את התוצאה — סכם בעברית: כמה הלכות חולצו, אילו שדות מטא-דאטה הושלמו, ו-status לכל פסיקה). +4. סמן את ה-issue כ-`done`. + +**אל**: אל תיצור issues של ביצוע בתיקי ערר, אל תיכנס לתהליך כתיבת החלטה — זו רק עבודת תחזוקה של ספריית הפסיקה. + ### שלב A: בדיקת מצב — שלמות, בדיקות שליליות, תאימות מתודולוגיה בכל heartbeat **רגיל** (לא comment routing): diff --git a/web/app.py b/web/app.py index 43b66a8..23e9fdd 100644 --- a/web/app.py +++ b/web/app.py @@ -48,6 +48,7 @@ from web.paperclip_client import ( post_comment as pc_post_comment, restore_project as pc_restore_project, wake_ceo_agent as pc_wake_ceo, + wake_for_precedent_extraction as pc_wake_for_precedent_extraction, ) @@ -3625,7 +3626,7 @@ async def precedent_library_upload( async def _run(): try: - await plib_service.ingest_precedent( + result = await plib_service.ingest_precedent( file_path=staged, citation=citation.strip(), case_name=case_name.strip(), @@ -3641,6 +3642,19 @@ async def precedent_library_upload( summary=summary.strip(), progress=publish, ) + # Auto-trigger Claude (via Paperclip) to extract halachot+metadata. + # Best-effort — failures are logged but don't surface to the user; + # `precedent_process_pending` can always be run manually. + case_law_id = result.get("case_law_id") if isinstance(result, dict) else None + if case_law_id: + try: + await pc_wake_for_precedent_extraction( + case_law_id=case_law_id, + citation=citation.strip(), + practice_area=practice_area, + ) + except Exception: + logger.exception("precedent-extraction wakeup failed (non-fatal)") except Exception as e: logger.exception("precedent-library upload failed") await _progress.set(task_id, { diff --git a/web/paperclip_client.py b/web/paperclip_client.py index 204f182..f59ed3e 100644 --- a/web/paperclip_client.py +++ b/web/paperclip_client.py @@ -632,6 +632,151 @@ async def post_comment(issue_id: str, company_id: str, body: str) -> dict: return {"comment_id": comment_id, "issue_id": issue_id, "method": "db_fallback"} +# Singleton project for the precedent-library extraction queue. One issue per +# uploaded precedent — assigned to the CEO who runs the local-MCP extractor. +_LIBRARY_PROJECT_NAME = "ספריית פסיקה — תור חילוץ" + + +async def _get_or_create_library_project( + conn: asyncpg.Connection, company_id: str, +) -> str: + """Return the project_id for the per-company library extraction queue, + creating it (with a workspace pointing at legal-ai) if it doesn't exist.""" + row = await conn.fetchrow( + "SELECT id FROM projects WHERE company_id = $1::uuid AND name = $2 LIMIT 1", + company_id, _LIBRARY_PROJECT_NAME, + ) + if row: + return str(row["id"]) + + project_id = str(uuid.uuid4()) + await conn.execute( + """INSERT INTO projects (id, company_id, name, description, status, color) + VALUES ($1, $2::uuid, $3, $4, 'backlog', $5)""", + project_id, company_id, _LIBRARY_PROJECT_NAME, + "תור אוטומטי לחילוץ הלכות ומטא-דאטה מפסיקה שהועלתה לספריה. " + "כל issue כאן מייצג פסק דין שצריך לעבד — להריץ " + "mcp__legal-ai__precedent_process_pending.", + "#a17a3a", # gold-ish + ) + await _ensure_default_workspace(conn, project_id, company_id) + logger.info("Created library extraction project %s for company %s", project_id, company_id) + return project_id + + +async def wake_for_precedent_extraction( + case_law_id: str, + citation: str, + practice_area: str = "", +) -> dict: + """Trigger Claude/Paperclip to run halacha+metadata extraction for a + freshly-uploaded precedent. + + Creates a Paperclip issue under the per-company "ספריית פסיקה" project, + assigns it to the company CEO, links the case_law_id via plugin_state, + and wakes the CEO via the Board API. The CEO instructions tell it to + run `mcp__legal-ai__precedent_process_pending` and close the issue. + + Best-effort: any failure is logged and swallowed so a partial Paperclip + outage doesn't block the upload itself. The user can always invoke + `precedent_process_pending` manually. + """ + if not PAPERCLIP_BOARD_API_KEY: + logger.warning( + "PAPERCLIP_BOARD_API_KEY not set — skipping precedent-extraction wakeup" + ) + return {"ok": False, "skipped": "no_api_key"} + + company_id = await _get_company_id(practice_area) + ceo_id = CEO_AGENTS.get(company_id, CEO_AGENT_ID) + + try: + conn = await asyncpg.connect(PAPERCLIP_DB_URL) + try: + project_id = await _get_or_create_library_project(conn, company_id) + + # Bump issue counter & build identifier (matches _create_issue style). + 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_citation = (citation[:120] + "…") if len(citation) > 120 else citation + description = ( + f"פסק דין חדש הועלה לספריית הפסיקה.\n\n" + f"**case_law_id:** `{case_law_id}`\n" + f"**citation:** {citation}\n\n" + f"---\n\n" + f"**משימה:** הרץ את הכלי `mcp__legal-ai__precedent_process_pending` " + f"פעמיים — פעם עם `kind='metadata'` ופעם עם `kind='halacha'`. " + f"הכלי יעבד את כל הפסיקות בתור (כולל זו), כך שגם אם הופעל מאוחר " + f"יותר עבור פסיקות אחרות — אין בעיה.\n\n" + f"לאחר ריצה: סמן את ה-issue כ-done ופתח comment קצר עם מספר ההלכות " + f"שחולצו ושדות המטא-דאטה שהושלמו." + ) + 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', 'medium', + $6, $7, $8::uuid)""", + issue_id, company_id, project_id, + f"[ספרייה] חלץ הלכות: {short_citation}"[:200], + description, + issue_number, identifier, ceo_id, + ) + + # Link case_law_id via plugin_state so the agent can find it. + await conn.execute( + """INSERT INTO plugin_state + (plugin_id, scope_kind, scope_id, namespace, state_key, value_json) + VALUES ($1::uuid, 'issue', $2, 'default', 'precedent-case-law-id', $3::jsonb) + ON CONFLICT (plugin_id, scope_kind, scope_id, namespace, state_key) + DO UPDATE SET value_json = $3::jsonb""", + PLUGIN_ID, issue_id, json.dumps(case_law_id), + ) + finally: + await conn.close() + except Exception as e: + logger.exception("wake_for_precedent_extraction: DB step failed: %s", e) + return {"ok": False, "error": f"db: {e}"} + + # Wake the CEO. Per Paperclip rules: must use API + carry issueId in payload. + url = f"{PAPERCLIP_API_URL}/api/agents/{ceo_id}/wakeup" + payload = { + "source": "automation", + "triggerDetail": "precedent_library_upload", + "reason": f"precedent_extraction_{identifier}", + "payload": { + "issueId": issue_id, + "mutation": "precedent_extraction", + "caseLawId": case_law_id, + }, + } + try: + async with httpx.AsyncClient(timeout=15) as client: + resp = await client.post( + url, + json=payload, + headers={"Authorization": f"Bearer {PAPERCLIP_BOARD_API_KEY}"}, + ) + resp.raise_for_status() + result = resp.json() + logger.info( + "Precedent-extraction wakeup queued: issue=%s case_law_id=%s", + identifier, case_law_id, + ) + return {"ok": True, "issue_id": issue_id, "identifier": identifier, "wakeup": result} + except Exception as e: + logger.exception("wake_for_precedent_extraction: 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.