משטחי-משתמש לקורפוס היומונים: endpoints ב-FastAPI + דף UI נפרד /digests (לדפדוף, חיפוש, העלאה, וקישור לפסק המקורי). היומון נשאר מקור-משני המצביע על הפסק — אינו מצוטט בהחלטה (INV-DIG1) ואינו מחלץ הלכות (INV-DIG2). Backend (container-safe + local split): - digest_library: פוצל ל-create_pending_digest (CONTAINER-SAFE: stage+ extract_text+create row 'pending', בלי LLM) ↔ enrich_digest/ process_pending_digests (local: LLM+embed+autolink). ingest_digest מאחד. - db.list_pending_digests; MCP digest_process_pending (tool+server) — חלופה ל-batch script לריקון התור. - web/app.py: 10 endpoints /api/digests/* (upload/list/search/queue-pending/ get/patch/delete/link/relink/unlink). upload=INSERT-only pending (ה-LLM רץ מקומית — claude_session local-only). כולם מחזירים dict בדפוס precedent. Frontend (Next 16, ללא api:types — hooks עם טיפוסים hand-written כמו precedent-library.ts): - lib/api/digests.ts — hooks (useDigests/useDigestSearch/useDigestPending/ useUploadDigest/useLink/Relink/Unlink/Delete/Update). - דף /digests נפרד (לא כרטיסייה ב-/precedents — לשמור גבול סמכותי/משני, INV-DIG1): טאבים יומונים/חיפוש + DigestCard (badge קישור-לפסק) + DigestUploadDialog + pending badge. nav + header-context. אומת: backend round-trip מלא (create_pending→list_pending→process_pending→ search→restore); web-ui מתקמפל (webpack/tsc נקי, route /digests נוצר). הערה: build דיפולטי (turbopack) נכשל ב-worktree עקב symlink ל-node_modules — ב-CI/Docker (node_modules אמיתי) עובד; אומת עם --webpack. Invariants: מקיים INV-DIG1/2 (upload לא מחלץ הלכות, UI מציג "מצביע לא מצוטט"), INV-DIG3 (link/relink/queue). G4 (אין בליעה — שגיאות→toast/HTTP), G2 (מסלול נפרד, לא מקביל). X6 (חוזה UI↔API — endpoints בדפוס precedent; hooks hand-written כמו שאר ה-domain modules). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
173 lines
6.3 KiB
Python
173 lines
6.3 KiB
Python
"""MCP tools for the Digests radar (X12).
|
|
|
|
A digest ("כל יום" daily one-pager, Ofer Toister) is a SECONDARY, discovery-
|
|
layer source that POINTS at a ruling. It is distinct from the three citation
|
|
corpora:
|
|
|
|
- ``search_precedent_library`` — authoritative external court rulings.
|
|
- ``search_internal_decisions`` — appeals-committee decisions.
|
|
- ``search_decisions`` — Dafna's prior decisions (style corpus).
|
|
|
|
A digest is NEVER cited in a decision (INV-DIG1) and NEVER enters the halacha
|
|
pipeline (INV-DIG2). ``search_digests`` is a research compass: it surfaces the
|
|
relevant digest + the UNDERLYING ruling's citation, which is then ingested into
|
|
the precedent library and cited from there.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from uuid import UUID
|
|
|
|
from legal_mcp.services import db, digest_library, telemetry
|
|
from legal_mcp.tools.envelope import empty, err as _err, ok as _ok
|
|
|
|
|
|
async def digest_upload(
|
|
file_path: str,
|
|
yomon_number: str = "",
|
|
digest_date: str = "",
|
|
practice_area: str = "",
|
|
appeal_subtype: str = "",
|
|
subject_tags: list[str] | None = None,
|
|
) -> str:
|
|
"""העלאת יומון ("כל יום") לקורפוס-הגילוי + חילוץ מטא-דאטה אוטומטי.
|
|
|
|
היומון הוא מקור-משני המצביע על פסק הדין המקורי — אינו מצוטט בהחלטה.
|
|
Args:
|
|
file_path: נתיב מלא לקובץ PDF/DOCX של היומון.
|
|
yomon_number: מספר היומון (אופציונלי — יחולץ מהטקסט אם ריק).
|
|
digest_date: ISO date של גיליון היומון (אופציונלי).
|
|
practice_area: rishuy_uvniya / betterment_levy / compensation_197.
|
|
subject_tags: תגיות נושא (אופציונלי — יחולצו אם ריק).
|
|
Returns: JSON עם digest_id, מספר היומון, מראה-המקום, וקישור-אוטומטי אם נמצא.
|
|
"""
|
|
try:
|
|
result = await digest_library.ingest_digest(
|
|
file_path=file_path,
|
|
yomon_number=yomon_number,
|
|
digest_date=digest_date or None,
|
|
practice_area=practice_area,
|
|
appeal_subtype=appeal_subtype,
|
|
subject_tags=subject_tags or None,
|
|
)
|
|
except Exception as e:
|
|
return _err(str(e))
|
|
return _ok(result)
|
|
|
|
|
|
async def digest_list(
|
|
practice_area: str = "",
|
|
concept_tag: str = "",
|
|
linked: bool | None = None,
|
|
search: str = "",
|
|
limit: int = 100,
|
|
) -> str:
|
|
"""רשימת יומונים בקורפוס-הגילוי, עם פילטרים. linked=false → יומונים שהפסק
|
|
המקורי שלהם עוד לא נקלט לספריית הפסיקה (פער-ידע גלוי)."""
|
|
rows = await digest_library.list_digests(
|
|
practice_area=practice_area,
|
|
concept_tag=concept_tag,
|
|
linked=linked,
|
|
search=search,
|
|
limit=limit,
|
|
)
|
|
return _ok(rows)
|
|
|
|
|
|
async def digest_get(digest_id: str) -> str:
|
|
"""יומון ספציפי לפי מזהה."""
|
|
try:
|
|
cid = UUID(digest_id)
|
|
except ValueError:
|
|
return _err("digest_id לא תקין")
|
|
record = await digest_library.get_digest(cid)
|
|
if not record:
|
|
return _err("יומון לא נמצא")
|
|
return _ok(record)
|
|
|
|
|
|
async def digest_link(digest_id: str, case_law_id: str) -> str:
|
|
"""קישור ידני של יומון לפסק הדין המקורי בספריית הפסיקה (INV-DIG3)."""
|
|
try:
|
|
UUID(digest_id)
|
|
UUID(case_law_id)
|
|
except ValueError:
|
|
return _err("מזהה לא תקין")
|
|
try:
|
|
result = await digest_library.link_digest(digest_id, case_law_id)
|
|
except Exception as e:
|
|
return _err(str(e))
|
|
return _ok(result)
|
|
|
|
|
|
async def digest_relink(digest_id: str) -> str:
|
|
"""ניסיון-קישור מחדש: בודק אם פסק הדין המקורי של היומון כבר בספרייה ומקשר."""
|
|
try:
|
|
UUID(digest_id)
|
|
except ValueError:
|
|
return _err("digest_id לא תקין")
|
|
try:
|
|
result = await digest_library.relink_digest(digest_id)
|
|
except Exception as e:
|
|
return _err(str(e))
|
|
return _ok(result)
|
|
|
|
|
|
async def digest_delete(digest_id: str) -> str:
|
|
"""מחיקת יומון מקורפוס-הגילוי."""
|
|
try:
|
|
cid = UUID(digest_id)
|
|
except ValueError:
|
|
return _err("digest_id לא תקין")
|
|
ok_ = await digest_library.delete_digest(cid)
|
|
if not ok_:
|
|
return _err("יומון לא נמצא")
|
|
return _ok({"deleted": True, "digest_id": digest_id})
|
|
|
|
|
|
async def search_digests(
|
|
query: str,
|
|
practice_area: str = "",
|
|
subject_tag: str = "",
|
|
concept_tag: str = "",
|
|
limit: int = 10,
|
|
) -> str:
|
|
"""חיפוש סמנטי בקורפוס-הגילוי (יומוני "כל יום"). מצפן-מחקר בלבד — מחזיר את
|
|
היומון הרלוונטי + מראה-המקום של הפסק המקורי (radar). היומון אינו מצוטט
|
|
בהחלטה (INV-DIG1); הצטט מהפסק המקורי דרך search_precedent_library."""
|
|
if not query or len(query.strip()) < 2:
|
|
return empty("שאילתה קצרה מדי (פחות מ-2 תווים).")
|
|
q = query.strip()
|
|
t0 = time.perf_counter()
|
|
results = await digest_library.search_digests(
|
|
query=q,
|
|
practice_area=practice_area,
|
|
subject_tag=subject_tag,
|
|
concept_tag=concept_tag,
|
|
limit=limit,
|
|
)
|
|
elapsed_ms = int((time.perf_counter() - t0) * 1000)
|
|
telemetry.log_search_bg(
|
|
search_type="digests",
|
|
query=q,
|
|
results=results,
|
|
duration_ms=elapsed_ms,
|
|
practice_area=practice_area or None,
|
|
user_agent="unknown",
|
|
)
|
|
if not results:
|
|
return empty("לא נמצאו יומונים תואמים.")
|
|
return _ok(results)
|
|
|
|
|
|
async def digest_process_pending(limit: int = 20) -> str:
|
|
"""ריקון תור היומונים שהועלו מה-UI וממתינים לעיבוד-LLM מקומי. מריץ חילוץ-
|
|
מטא-דאטה + embedding + autolink על כל יומון בסטטוס 'pending', מקומית עם
|
|
ה-CLI (claude_session local-only). מנקה לסטטוס 'completed'."""
|
|
try:
|
|
result = await digest_library.process_pending_digests(limit=limit)
|
|
except Exception as e:
|
|
return _err(str(e))
|
|
return _ok(result)
|