feat(precedents): auto-trigger Claude extraction via Paperclip wakeup
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 16:49:25 +00:00
parent da0a385d9c
commit 923903217c
3 changed files with 183 additions and 1 deletions

View File

@@ -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):

View File

@@ -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, {

View File

@@ -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.