feat(mcp): FU-14 GAP-48 פרוסה 2 — envelope אחיד ל-11 משפחות-כלים
המשך מיגרציית INV-TOOL1 מעבר למשפחת-החיפוש (#71). הומרו ל-{status,data,message}: precedent_library, citations, internal_decisions, missing_precedents, training_enrichment, precedents, legal_arguments, cases, documents, workflow (~55 כלים). בוטלו 5 עותקי _ok/_err משוכפלים (alias ל-tools/envelope.py — SSoT, G2). עיקרון: envelope-status = הצלחת-הקריאה-לכלי; תוצאה-עסקית (idempotent_existing, noop, completed...) נשמרת בתוך data. err רק לכשל אמיתי (not-found/invalid/exception). תאימות-API: צרכני web/app.py של cases/workflow/precedents חוּוטו דרך envelope_unwrap + בדיקת status=="error"→4xx — תשובת ה-HTTP זהה, web-ui לא מושפע. (documents/legal_arguments/citations/... אינם נצרכים מ-app.py — agent-only.) בדיקות: 182/182 עוברים (test_corpus_constraints עודכן לחוזה החדש). נותר: משפחת drafting (מסלול הפקת-ההחלטה) בפרוסה נפרדת עם שער טסט-ייצוא. Invariants: מקדם INV-TOOL1 + G2 (SSoT, ביטול כפילות). מתועד ב-X9 + gap-audit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from uuid import UUID
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import audit, db, git_sync, processor
|
||||
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
|
||||
|
||||
|
||||
async def document_upload(
|
||||
@@ -28,11 +29,11 @@ async def document_upload(
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
source = Path(file_path)
|
||||
if not source.exists():
|
||||
return f"קובץ לא נמצא: {file_path}"
|
||||
return err(f"קובץ לא נמצא: {file_path}")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
if not title:
|
||||
@@ -44,12 +45,10 @@ async def document_upload(
|
||||
content_hash = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
existing_doc = await db.get_document_by_hash(case_id, content_hash)
|
||||
if existing_doc:
|
||||
return json.dumps({
|
||||
"status": "exists",
|
||||
"message": f"הקובץ כבר הועלה לתיק {case_number} (זהה ב-hash) — מוחזר הקיים, ללא עיבוד מחדש.",
|
||||
return ok({
|
||||
"document": existing_doc,
|
||||
"idempotent_existing": True,
|
||||
}, ensure_ascii=False, indent=2, default=str)
|
||||
}, message=f"הקובץ כבר הועלה לתיק {case_number} (זהה ב-hash) — מוחזר הקיים, ללא עיבוד מחדש.")
|
||||
|
||||
# Copy file to case directory
|
||||
case_dir = config.find_case_dir(case_number) / "documents" / "originals"
|
||||
@@ -106,10 +105,10 @@ async def document_upload(
|
||||
"document_upload", case_id=case_id, document_id=UUID(doc["id"]),
|
||||
details={"title": title, "doc_type": actual_doc_type},
|
||||
)
|
||||
return json.dumps({
|
||||
return ok({
|
||||
"document": doc,
|
||||
"processing": result,
|
||||
}, default=str, ensure_ascii=False, indent=2)
|
||||
})
|
||||
|
||||
|
||||
async def document_upload_training(
|
||||
@@ -139,7 +138,7 @@ async def document_upload_training(
|
||||
|
||||
source = Path(file_path)
|
||||
if not source.exists():
|
||||
return f"קובץ לא נמצא: {file_path}"
|
||||
return err(f"קובץ לא נמצא: {file_path}")
|
||||
|
||||
if not title:
|
||||
title = source.stem
|
||||
@@ -214,13 +213,13 @@ async def document_upload_training(
|
||||
]
|
||||
await db.store_chunks(doc_id, None, chunk_dicts)
|
||||
|
||||
return json.dumps({
|
||||
return ok({
|
||||
"corpus_id": str(corpus_id),
|
||||
"title": title,
|
||||
"pages": page_count,
|
||||
"text_length": len(text),
|
||||
"chunks": len(chunks) if chunks else 0,
|
||||
}, default=str, ensure_ascii=False, indent=2)
|
||||
})
|
||||
|
||||
|
||||
async def document_get_text(case_number: str, doc_title: str = "") -> str:
|
||||
@@ -232,16 +231,16 @@ async def document_get_text(case_number: str, doc_title: str = "") -> str:
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
docs = await db.list_documents(UUID(case["id"]))
|
||||
if not docs:
|
||||
return f"אין מסמכים בתיק {case_number}."
|
||||
return empty(f"אין מסמכים בתיק {case_number}.")
|
||||
|
||||
if doc_title:
|
||||
docs = [d for d in docs if doc_title.lower() in d["title"].lower()]
|
||||
if not docs:
|
||||
return f"מסמך '{doc_title}' לא נמצא בתיק."
|
||||
return err(f"מסמך '{doc_title}' לא נמצא בתיק.")
|
||||
|
||||
results = []
|
||||
for doc in docs:
|
||||
@@ -252,7 +251,7 @@ async def document_get_text(case_number: str, doc_title: str = "") -> str:
|
||||
"text": text[:10000] if text else "(ללא טקסט)",
|
||||
})
|
||||
|
||||
return json.dumps(results, ensure_ascii=False, indent=2)
|
||||
return ok(results)
|
||||
|
||||
|
||||
async def document_list(case_number: str) -> str:
|
||||
@@ -263,13 +262,13 @@ async def document_list(case_number: str) -> str:
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
docs = await db.list_documents(UUID(case["id"]))
|
||||
if not docs:
|
||||
return f"אין מסמכים בתיק {case_number}."
|
||||
return empty(f"אין מסמכים בתיק {case_number}.")
|
||||
|
||||
return json.dumps(docs, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(docs)
|
||||
|
||||
|
||||
async def extract_references(
|
||||
@@ -286,12 +285,12 @@ async def extract_references(
|
||||
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
docs = await db.list_documents(case_id)
|
||||
if not docs:
|
||||
return f"אין מסמכים בתיק {case_number}."
|
||||
return empty(f"אין מסמכים בתיק {case_number}.")
|
||||
|
||||
if doc_title:
|
||||
docs = [d for d in docs if doc_title.lower() in d["title"].lower()]
|
||||
@@ -313,7 +312,7 @@ async def extract_references(
|
||||
"legislation": refs["legislation"],
|
||||
})
|
||||
|
||||
return json.dumps(results, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(results)
|
||||
|
||||
|
||||
async def extract_claims(
|
||||
@@ -332,12 +331,12 @@ async def extract_claims(
|
||||
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
docs = await db.list_documents(case_id)
|
||||
if not docs:
|
||||
return f"אין מסמכים בתיק {case_number}."
|
||||
return empty(f"אין מסמכים בתיק {case_number}.")
|
||||
|
||||
# Filter to claims documents (appeal, response) or specific doc
|
||||
if doc_title:
|
||||
@@ -346,7 +345,7 @@ async def extract_claims(
|
||||
docs = [d for d in docs if d["doc_type"] in ("appeal", "response", "objection")]
|
||||
|
||||
if not docs:
|
||||
return "לא נמצאו כתבי טענות בתיק."
|
||||
return empty("לא נמצאו כתבי טענות בתיק.")
|
||||
|
||||
results = []
|
||||
for doc in docs:
|
||||
@@ -367,7 +366,7 @@ async def extract_claims(
|
||||
"extract_claims", case_id=case_id,
|
||||
details={"docs_processed": len(docs), "results": len(results)},
|
||||
)
|
||||
return json.dumps(results, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(results)
|
||||
|
||||
|
||||
async def get_claims(case_number: str, party_role: str = "") -> str:
|
||||
@@ -379,7 +378,7 @@ async def get_claims(case_number: str, party_role: str = "") -> str:
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
claims = await db.get_claims(
|
||||
UUID(case["id"]),
|
||||
@@ -387,7 +386,7 @@ async def get_claims(case_number: str, party_role: str = "") -> str:
|
||||
)
|
||||
|
||||
if not claims:
|
||||
return f"אין טענות בתיק {case_number}."
|
||||
return empty(f"אין טענות בתיק {case_number}.")
|
||||
|
||||
# Format for display
|
||||
role_hebrew = {
|
||||
@@ -405,7 +404,7 @@ async def get_claims(case_number: str, party_role: str = "") -> str:
|
||||
"source": c.get("source_document", ""),
|
||||
})
|
||||
|
||||
return json.dumps(formatted, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(formatted)
|
||||
|
||||
|
||||
# Whitelist of doc_type values; mirrors web/app.py:DOC_TYPE_NAMES.
|
||||
@@ -440,37 +439,26 @@ async def document_update(
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"תיק {case_number} לא נמצא."},
|
||||
ensure_ascii=False, indent=2)
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
try:
|
||||
doc_uuid = UUID(doc_id)
|
||||
except ValueError:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"doc_id לא תקין: {doc_id}"},
|
||||
ensure_ascii=False, indent=2)
|
||||
return err(f"doc_id לא תקין: {doc_id}")
|
||||
|
||||
doc = await db.get_document(doc_uuid)
|
||||
if not doc:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"מסמך {doc_id} לא נמצא."},
|
||||
ensure_ascii=False, indent=2)
|
||||
return err(f"מסמך {doc_id} לא נמצא.")
|
||||
|
||||
if doc.get("case_id") != case["id"]:
|
||||
return json.dumps({"status": "error",
|
||||
"message": f"מסמך {doc_id} לא שייך לתיק {case_number}."},
|
||||
ensure_ascii=False, indent=2)
|
||||
return err(f"מסמך {doc_id} לא שייך לתיק {case_number}.")
|
||||
|
||||
updates: dict = {}
|
||||
|
||||
if doc_type:
|
||||
if doc_type not in ALLOWED_DOC_TYPES:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"message": f"doc_type לא תקין: {doc_type}",
|
||||
"allowed": sorted(ALLOWED_DOC_TYPES),
|
||||
}, ensure_ascii=False, indent=2)
|
||||
return err(f"doc_type לא תקין: {doc_type}",
|
||||
data={"allowed": sorted(ALLOWED_DOC_TYPES)})
|
||||
updates["doc_type"] = doc_type
|
||||
|
||||
# appraiser_side is optional. The MCP tool can't distinguish "skip" from
|
||||
@@ -478,11 +466,8 @@ async def document_update(
|
||||
# To clear, the operator must edit metadata directly (rare).
|
||||
if appraiser_side:
|
||||
if appraiser_side not in ALLOWED_APPRAISER_SIDES:
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"message": f"appraiser_side לא תקין: {appraiser_side}",
|
||||
"allowed": sorted(s for s in ALLOWED_APPRAISER_SIDES if s),
|
||||
}, ensure_ascii=False, indent=2)
|
||||
return err(f"appraiser_side לא תקין: {appraiser_side}",
|
||||
data={"allowed": sorted(s for s in ALLOWED_APPRAISER_SIDES if s)})
|
||||
metadata = doc.get("metadata") or {}
|
||||
if isinstance(metadata, str):
|
||||
metadata = json.loads(metadata)
|
||||
@@ -490,14 +475,12 @@ async def document_update(
|
||||
updates["metadata"] = metadata
|
||||
|
||||
if not updates:
|
||||
return json.dumps({"status": "noop", "message": "אין שינוי לבצע."},
|
||||
ensure_ascii=False, indent=2)
|
||||
return ok({"noop": True}, message="אין שינוי לבצע.")
|
||||
|
||||
await db.update_document(doc_uuid, **updates)
|
||||
fresh = await db.get_document(doc_uuid)
|
||||
return json.dumps({
|
||||
"status": "completed",
|
||||
return ok({
|
||||
"doc_id": doc_id,
|
||||
"doc_type": fresh.get("doc_type"),
|
||||
"metadata": fresh.get("metadata"),
|
||||
}, default=str, ensure_ascii=False, indent=2)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user