feat(precedents): auto-trigger Claude extraction via Paperclip wakeup
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
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:
@@ -29,6 +29,11 @@ tools:
|
|||||||
- mcp__legal-ai__apply_user_edit
|
- mcp__legal-ai__apply_user_edit
|
||||||
- mcp__legal-ai__list_bookmarks
|
- mcp__legal-ai__list_bookmarks
|
||||||
- mcp__legal-ai__revise_draft
|
- 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`):
|
**לפני כל דבר אחר** — בדוק את סיבת ההתעוררות (`$PAPERCLIP_WAKE_REASON`):
|
||||||
- אם ה-reason מכיל `user_commented` → **דלג ישירות לסעיף "טיפול בתגובות חדשות מחיים"**. אל תסרוק תיקים אחרים, אל תבדוק issues, אל תעשה heartbeat רגיל. **טפל רק בתגובה.**
|
- אם ה-reason מכיל `user_commented` → **דלג ישירות לסעיף "טיפול בתגובות חדשות מחיים"**. אל תסרוק תיקים אחרים, אל תבדוק issues, אל תעשה heartbeat רגיל. **טפל רק בתגובה.**
|
||||||
- אם ה-reason מכיל `agent_completion` → דלג לשלב E/F בהתאם לסוכן שסיים
|
- אם ה-reason מכיל `agent_completion` → דלג לשלב E/F בהתאם לסוכן שסיים
|
||||||
|
- אם ה-reason מכיל `precedent_extraction_` → **דלג לסעיף "חילוץ פסיקה אוטומטי"**. אל תיגע בתיקים — זו עבודת ספרייה.
|
||||||
- אחרת → המשך לשלב A (heartbeat רגיל)
|
- אחרת → המשך לשלב 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: בדיקת מצב — שלמות, בדיקות שליליות, תאימות מתודולוגיה
|
### שלב A: בדיקת מצב — שלמות, בדיקות שליליות, תאימות מתודולוגיה
|
||||||
|
|
||||||
בכל heartbeat **רגיל** (לא comment routing):
|
בכל heartbeat **רגיל** (לא comment routing):
|
||||||
|
|||||||
16
web/app.py
16
web/app.py
@@ -48,6 +48,7 @@ from web.paperclip_client import (
|
|||||||
post_comment as pc_post_comment,
|
post_comment as pc_post_comment,
|
||||||
restore_project as pc_restore_project,
|
restore_project as pc_restore_project,
|
||||||
wake_ceo_agent as pc_wake_ceo,
|
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():
|
async def _run():
|
||||||
try:
|
try:
|
||||||
await plib_service.ingest_precedent(
|
result = await plib_service.ingest_precedent(
|
||||||
file_path=staged,
|
file_path=staged,
|
||||||
citation=citation.strip(),
|
citation=citation.strip(),
|
||||||
case_name=case_name.strip(),
|
case_name=case_name.strip(),
|
||||||
@@ -3641,6 +3642,19 @@ async def precedent_library_upload(
|
|||||||
summary=summary.strip(),
|
summary=summary.strip(),
|
||||||
progress=publish,
|
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:
|
except Exception as e:
|
||||||
logger.exception("precedent-library upload failed")
|
logger.exception("precedent-library upload failed")
|
||||||
await _progress.set(task_id, {
|
await _progress.set(task_id, {
|
||||||
|
|||||||
@@ -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"}
|
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:
|
async def wake_ceo_agent(issue_id: str, case_number: str, company_id: str = "") -> dict:
|
||||||
"""Wake the CEO agent via Paperclip's wakeup API.
|
"""Wake the CEO agent via Paperclip's wakeup API.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user