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:
@@ -14,6 +14,7 @@ import httpx
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import audit, db, extractor, git_sync, practice_area as pa
|
||||
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -158,7 +159,7 @@ async def case_create(
|
||||
_existing = await db.get_case_by_number(case_number)
|
||||
if _existing:
|
||||
_existing["idempotent_existing"] = True
|
||||
return json.dumps(_existing, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(_existing)
|
||||
|
||||
from datetime import date as date_type
|
||||
|
||||
@@ -257,7 +258,7 @@ async def case_create(
|
||||
# silently producing a case with no remote.
|
||||
case["gitea"] = await _setup_gitea_remote(case_number, title, case_dir)
|
||||
|
||||
return json.dumps(case, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(case)
|
||||
|
||||
|
||||
async def case_list(status: str = "", limit: int = 50) -> str:
|
||||
@@ -272,8 +273,8 @@ async def case_list(status: str = "", limit: int = 50) -> str:
|
||||
"""
|
||||
cases = await db.list_cases(status=status or None, limit=limit)
|
||||
if not cases:
|
||||
return "אין תיקים."
|
||||
return json.dumps(cases, default=str, ensure_ascii=False, indent=2)
|
||||
return empty("אין תיקים.")
|
||||
return ok(cases)
|
||||
|
||||
|
||||
async def case_get(case_number: str) -> str:
|
||||
@@ -284,11 +285,11 @@ async def case_get(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"]))
|
||||
case["documents"] = docs
|
||||
return json.dumps(case, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(case)
|
||||
|
||||
|
||||
async def case_update(
|
||||
@@ -338,7 +339,7 @@ async def case_update(
|
||||
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
fields = {}
|
||||
if status:
|
||||
@@ -395,7 +396,7 @@ async def case_update(
|
||||
except Exception:
|
||||
pass # git not available — non-critical
|
||||
|
||||
return json.dumps(updated, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(updated)
|
||||
|
||||
|
||||
async def case_delete(case_number: str, remove_files: bool = False) -> str:
|
||||
@@ -408,28 +409,25 @@ async def case_delete(case_number: str, remove_files: bool = False) -> str:
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return json.dumps(
|
||||
{"deleted": False, "reason": f"תיק {case_number} לא נמצא."},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
ok = await db.delete_case(case_id)
|
||||
deleted = await db.delete_case(case_id)
|
||||
|
||||
result = {
|
||||
"deleted": ok,
|
||||
"deleted": deleted,
|
||||
"case_number": case_number,
|
||||
"case_id": str(case_id),
|
||||
"removed_files": False,
|
||||
}
|
||||
|
||||
if ok and remove_files:
|
||||
if deleted and remove_files:
|
||||
case_dir = config.find_case_dir(case_number)
|
||||
if case_dir.exists():
|
||||
shutil.rmtree(case_dir, ignore_errors=True)
|
||||
result["removed_files"] = True
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
return ok(result)
|
||||
|
||||
|
||||
async def case_get_final_text(case_number: str, max_chars: int = 0) -> str:
|
||||
@@ -456,27 +454,24 @@ async def case_get_final_text(case_number: str, max_chars: int = 0) -> str:
|
||||
break
|
||||
|
||||
if final_path is None:
|
||||
return json.dumps({
|
||||
"status": "not_found",
|
||||
"case_number": case_number,
|
||||
"expected_path": str(exports_dir / f"{final_stem}.docx"),
|
||||
"tried_extensions": [".docx", ".pdf", ".doc", ".rtf", ".txt", ".md"],
|
||||
"hint": (
|
||||
"ההחלטה הסופית עדיין לא סומנה כ'סופית' ב-UI. "
|
||||
"דפנה צריכה ללחוץ 'סמן כסופי' על קובץ הטיוטה הנכון."
|
||||
),
|
||||
}, ensure_ascii=False, indent=2)
|
||||
return err(
|
||||
"ההחלטה הסופית עדיין לא סומנה כ'סופית' ב-UI. "
|
||||
"דפנה צריכה ללחוץ 'סמן כסופי' על קובץ הטיוטה הנכון.",
|
||||
data={
|
||||
"case_number": case_number,
|
||||
"expected_path": str(exports_dir / f"{final_stem}.docx"),
|
||||
"tried_extensions": [".docx", ".pdf", ".doc", ".rtf", ".txt", ".md"],
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
text, page_count, _ = await extractor.extract_text(str(final_path))
|
||||
except Exception as e:
|
||||
logger.exception("case_get_final_text: extraction failed for %s", case_number)
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"case_number": case_number,
|
||||
"file_path": str(final_path),
|
||||
"error": str(e),
|
||||
}, ensure_ascii=False, indent=2)
|
||||
return err(
|
||||
f"חילוץ הטקסט נכשל: {e}",
|
||||
data={"case_number": case_number, "file_path": str(final_path)},
|
||||
)
|
||||
|
||||
text = text or ""
|
||||
truncated = False
|
||||
@@ -484,12 +479,11 @@ async def case_get_final_text(case_number: str, max_chars: int = 0) -> str:
|
||||
text = text[:max_chars]
|
||||
truncated = True
|
||||
|
||||
return json.dumps({
|
||||
"status": "ok",
|
||||
return ok({
|
||||
"case_number": case_number,
|
||||
"file_path": str(final_path),
|
||||
"text_length": len(text),
|
||||
"page_count": page_count,
|
||||
"truncated": truncated,
|
||||
"text": text,
|
||||
}, ensure_ascii=False, indent=2)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user