Merge pull request 'feat(mcp): FU-14 GAP-48 פרוסה 2 — envelope אחיד ל-11 משפחות-כלים' (#77) from fix/fu14-gap48-envelope-rest into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 2m5s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 2m5s
This commit was merged in pull request #77.
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)
|
||||
})
|
||||
|
||||
@@ -23,18 +23,10 @@ missing decision so that newer rows now link to it).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import citation_extractor
|
||||
|
||||
|
||||
def _ok(payload) -> str:
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
|
||||
def _err(msg: str) -> str:
|
||||
return json.dumps({"error": msg}, ensure_ascii=False)
|
||||
from legal_mcp.tools.envelope import err as _err, ok as _ok # GAP-48: SSoT envelope
|
||||
|
||||
|
||||
async def extract_internal_citations(
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -14,9 +14,8 @@ decisions and enforces the required metadata at the tool boundary.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from legal_mcp.services import internal_decisions as int_svc
|
||||
from legal_mcp.tools.envelope import err as _err, ok as _ok # GAP-48: SSoT envelope
|
||||
|
||||
# Valid Hebrew district names (matches _COURT_TO_DISTRICT in service)
|
||||
VALID_DISTRICTS = {"ירושלים", "מרכז", "תל אביב", "תל-אביב", "צפון", "דרום", "חיפה", "ארצי"}
|
||||
@@ -26,14 +25,6 @@ VALID_DISTRICTS = {"ירושלים", "מרכז", "תל אביב", "תל-אביב
|
||||
VALID_PROCEEDING_TYPES = {"ערר", 'בל"מ'}
|
||||
|
||||
|
||||
def _ok(payload) -> str:
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
|
||||
def _err(msg: str) -> str:
|
||||
return json.dumps({"error": msg}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def internal_decision_upload(
|
||||
file_path: str,
|
||||
case_number: str,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import argument_aggregator, db
|
||||
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
|
||||
|
||||
|
||||
async def aggregate_claims_to_arguments(
|
||||
@@ -20,17 +20,14 @@ async def aggregate_claims_to_arguments(
|
||||
"""
|
||||
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} לא נמצא.")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
result = await argument_aggregator.aggregate_claims_to_arguments(
|
||||
case_id, force=force,
|
||||
)
|
||||
result["case_number"] = case_number
|
||||
return json.dumps(result, ensure_ascii=False, indent=2, default=str)
|
||||
return ok(result)
|
||||
|
||||
|
||||
async def get_legal_arguments(
|
||||
@@ -46,21 +43,16 @@ async def get_legal_arguments(
|
||||
"""
|
||||
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} לא נמצא.")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
args = await argument_aggregator.get_legal_arguments(case_id, party=party)
|
||||
|
||||
if not args:
|
||||
return json.dumps({
|
||||
"status": "empty",
|
||||
"case_number": case_number,
|
||||
"message": "לא נמצאו טיעונים מאוגדים. הרץ aggregate_claims_to_arguments תחילה.",
|
||||
"arguments": [],
|
||||
}, ensure_ascii=False, indent=2)
|
||||
return empty(
|
||||
"לא נמצאו טיעונים מאוגדים. הרץ aggregate_claims_to_arguments תחילה.",
|
||||
data={"case_number": case_number, "arguments": []},
|
||||
)
|
||||
|
||||
# Group by party for nicer display.
|
||||
party_he = {
|
||||
@@ -75,9 +67,8 @@ async def get_legal_arguments(
|
||||
label = party_he.get(a["party"], a["party"])
|
||||
by_party.setdefault(label, []).append(a)
|
||||
|
||||
return json.dumps({
|
||||
"status": "ok",
|
||||
return ok({
|
||||
"case_number": case_number,
|
||||
"total": len(args),
|
||||
"by_party": by_party,
|
||||
}, ensure_ascii=False, indent=2, default=str)
|
||||
})
|
||||
|
||||
@@ -18,18 +18,10 @@ Three tools:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import db
|
||||
|
||||
|
||||
def _ok(payload) -> str:
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
|
||||
def _err(msg: str) -> str:
|
||||
return json.dumps({"error": msg}, ensure_ascii=False)
|
||||
from legal_mcp.tools.envelope import err as _err, ok as _ok # GAP-48: SSoT envelope
|
||||
|
||||
|
||||
async def _resolve_case_id(case_number: str) -> UUID | None:
|
||||
|
||||
@@ -17,19 +17,11 @@ the chair approves them — per project review policy.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import db, precedent_library, telemetry
|
||||
|
||||
|
||||
def _ok(payload) -> str:
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
|
||||
def _err(msg: str) -> str:
|
||||
return json.dumps({"error": msg}, ensure_ascii=False)
|
||||
from legal_mcp.tools.envelope import empty, err as _err, ok as _ok # GAP-48: SSoT envelope
|
||||
|
||||
|
||||
async def precedent_library_upload(
|
||||
@@ -293,7 +285,7 @@ async def search_precedent_library(
|
||||
Returns: רשימה מדורגת. כל פריט הוא {"type": "halacha"|"passage", "score", ...}.
|
||||
"""
|
||||
if not query or len(query.strip()) < 2:
|
||||
return json.dumps([], ensure_ascii=False)
|
||||
return empty("שאילתה קצרה מדי (פחות מ-2 תווים).")
|
||||
q = query.strip()
|
||||
t0 = time.perf_counter()
|
||||
results = await precedent_library.search_library(
|
||||
|
||||
@@ -7,11 +7,10 @@ free-text citations the chair attaches during the compose phase.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import db
|
||||
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
|
||||
|
||||
|
||||
async def precedent_attach(
|
||||
@@ -34,14 +33,14 @@ async def precedent_attach(
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return json.dumps({"error": f"תיק {case_number} לא נמצא."}, ensure_ascii=False)
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
pdf_uuid: UUID | None = None
|
||||
if pdf_document_id:
|
||||
try:
|
||||
pdf_uuid = UUID(pdf_document_id)
|
||||
except ValueError:
|
||||
return json.dumps({"error": "pdf_document_id לא תקין"}, ensure_ascii=False)
|
||||
return err("pdf_document_id לא תקין")
|
||||
|
||||
# INV-TOOL3 / GAP-52: idempotent on (case_id, section_id, citation, quote).
|
||||
# Re-attaching the same quote to the same section returns the existing row.
|
||||
@@ -49,7 +48,7 @@ async def precedent_attach(
|
||||
if (_p.get("citation") == citation and _p.get("quote") == quote
|
||||
and (_p.get("section_id") or None) == (section_id or None)):
|
||||
_p["idempotent_existing"] = True
|
||||
return json.dumps(_p, ensure_ascii=False, indent=2, default=str)
|
||||
return ok(_p)
|
||||
|
||||
row = await db.create_case_precedent(
|
||||
case_id=UUID(case["id"]),
|
||||
@@ -60,17 +59,17 @@ async def precedent_attach(
|
||||
pdf_document_id=pdf_uuid,
|
||||
practice_area=case.get("practice_area"),
|
||||
)
|
||||
return json.dumps(row, ensure_ascii=False, indent=2, default=str)
|
||||
return ok(row)
|
||||
|
||||
|
||||
async def precedent_list(case_number: str) -> str:
|
||||
"""רשימת כל הפסיקות שצורפו לתיק, ממוינות לפי סעיף ואז לפי זמן יצירה."""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return json.dumps({"error": f"תיק {case_number} לא נמצא."}, ensure_ascii=False)
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
rows = await db.list_case_precedents(UUID(case["id"]))
|
||||
return json.dumps(rows, ensure_ascii=False, indent=2, default=str)
|
||||
return ok(rows)
|
||||
|
||||
|
||||
async def precedent_remove(precedent_id: str) -> str:
|
||||
@@ -78,12 +77,10 @@ async def precedent_remove(precedent_id: str) -> str:
|
||||
try:
|
||||
pid = UUID(precedent_id)
|
||||
except ValueError:
|
||||
return json.dumps({"error": "precedent_id לא תקין"}, ensure_ascii=False)
|
||||
return err("precedent_id לא תקין")
|
||||
|
||||
ok = await db.delete_case_precedent(pid)
|
||||
return json.dumps(
|
||||
{"deleted": ok, "precedent_id": precedent_id}, ensure_ascii=False,
|
||||
)
|
||||
deleted = await db.delete_case_precedent(pid)
|
||||
return ok({"deleted": deleted, "precedent_id": precedent_id})
|
||||
|
||||
|
||||
async def precedent_search_library(
|
||||
@@ -97,7 +94,7 @@ async def precedent_search_library(
|
||||
limit: מספר תוצאות מקסימלי
|
||||
"""
|
||||
if not query or len(query.strip()) < 2:
|
||||
return json.dumps([], ensure_ascii=False)
|
||||
return empty("שאילתה קצרה מדי (פחות מ-2 תווים).")
|
||||
|
||||
rows = await db.search_precedent_library(query.strip(), practice_area, limit)
|
||||
return json.dumps(rows, ensure_ascii=False, indent=2, default=str)
|
||||
return ok(rows)
|
||||
|
||||
@@ -15,18 +15,10 @@ CLI is available, and the row gets enriched.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp.services import db, style_metadata_extractor
|
||||
|
||||
|
||||
def _ok(payload) -> str:
|
||||
return json.dumps({"ok": True, **payload}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
def _err(msg: str) -> str:
|
||||
return json.dumps({"ok": False, "error": msg}, ensure_ascii=False)
|
||||
from legal_mcp.tools.envelope import err as _err, ok as _ok # GAP-48: SSoT envelope
|
||||
|
||||
|
||||
async def extract_decision_metadata(corpus_id: str, overwrite: bool = False) -> str:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
@@ -12,6 +11,7 @@ from legal_mcp.services.lessons import (
|
||||
VALID_OUTCOMES,
|
||||
canonical_outcome,
|
||||
)
|
||||
from legal_mcp.tools.envelope import empty, err, ok # GAP-48: SSoT envelope
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,7 +24,7 @@ async def workflow_status(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} לא נמצא.")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
docs = await db.list_documents(case_id)
|
||||
@@ -69,7 +69,7 @@ async def workflow_status(case_number: str) -> str:
|
||||
"next_steps": _suggest_next_steps(case, docs, has_draft),
|
||||
}
|
||||
|
||||
return json.dumps(status, ensure_ascii=False, indent=2)
|
||||
return ok(status)
|
||||
|
||||
|
||||
def _suggest_next_steps(case: dict, docs: list, has_draft: bool) -> list[str]:
|
||||
@@ -114,12 +114,12 @@ async def get_metrics(case_number: str = "") -> str:
|
||||
if case_number:
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
result = await metrics.get_case_metrics(UUID(case["id"]))
|
||||
else:
|
||||
result = await metrics.get_dashboard()
|
||||
|
||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(result)
|
||||
|
||||
|
||||
async def processing_status() -> str:
|
||||
@@ -135,14 +135,14 @@ async def processing_status() -> str:
|
||||
corpus_count = await conn.fetchval("SELECT COUNT(*) FROM style_corpus")
|
||||
pattern_count = await conn.fetchval("SELECT COUNT(*) FROM style_patterns")
|
||||
|
||||
return json.dumps({
|
||||
return ok({
|
||||
"cases": case_count,
|
||||
"documents": doc_count,
|
||||
"pending_processing": pending_count,
|
||||
"chunks": chunk_count,
|
||||
"style_corpus_entries": corpus_count,
|
||||
"style_patterns": pattern_count,
|
||||
}, ensure_ascii=False, indent=2)
|
||||
})
|
||||
|
||||
|
||||
# ── Outcome & Brainstorming ───────────────────────────────────────
|
||||
@@ -164,12 +164,12 @@ async def set_outcome(
|
||||
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
return err(f"תיק {case_number} לא נמצא.")
|
||||
|
||||
# GAP-51: accept legacy vocabulary (rejected/accepted/partial), store canonical.
|
||||
outcome = canonical_outcome(outcome)
|
||||
if outcome not in VALID_OUTCOMES:
|
||||
return f"תוצאה לא תקינה. אפשרויות: {', '.join(VALID_OUTCOMES)}"
|
||||
return err(f"תוצאה לא תקינה. אפשרויות: {', '.join(VALID_OUTCOMES)}")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
|
||||
@@ -211,7 +211,7 @@ async def set_outcome(
|
||||
result["message"] = f"תוצאה נשמרה: {outcome_hebrew}. ניתן להתחיל כתיבת טיוטה."
|
||||
result["next_step"] = "draft"
|
||||
|
||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(result)
|
||||
|
||||
|
||||
async def brainstorm_directions(
|
||||
@@ -226,14 +226,14 @@ async def brainstorm_directions(
|
||||
|
||||
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"])
|
||||
|
||||
# Get existing decision for outcome
|
||||
decision = await db.get_decision_by_case(case_id)
|
||||
if not decision:
|
||||
return "לא הוזנה תוצאה לתיק. הפעל set_outcome קודם."
|
||||
return err("לא הוזנה תוצאה לתיק. הפעל set_outcome קודם.")
|
||||
|
||||
outcome = decision.get("outcome", "")
|
||||
reasoning = decision.get("outcome_reasoning", "")
|
||||
@@ -246,7 +246,7 @@ async def brainstorm_directions(
|
||||
direction_doc={"brainstorm": directions, "approved": False},
|
||||
)
|
||||
|
||||
return json.dumps(directions, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(directions)
|
||||
|
||||
|
||||
async def approve_direction(
|
||||
@@ -265,18 +265,18 @@ async def approve_direction(
|
||||
|
||||
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"])
|
||||
decision = await db.get_decision_by_case(case_id)
|
||||
if not decision:
|
||||
return "לא הוזנה תוצאה לתיק."
|
||||
return err("לא הוזנה תוצאה לתיק.")
|
||||
|
||||
direction_data = decision.get("direction_doc") or {}
|
||||
brainstorm_result = direction_data.get("brainstorm", {})
|
||||
|
||||
if not brainstorm_result.get("directions"):
|
||||
return "לא בוצע סיעור מוחות. הפעל brainstorm_directions קודם."
|
||||
return err("לא בוצע סיעור מוחות. הפעל brainstorm_directions קודם.")
|
||||
|
||||
direction_doc = brainstorm.build_direction_doc(
|
||||
outcome=decision.get("outcome", ""),
|
||||
@@ -288,11 +288,8 @@ async def approve_direction(
|
||||
|
||||
await db.update_decision(UUID(decision["id"]), direction_doc=direction_doc)
|
||||
|
||||
return json.dumps({
|
||||
"status": "approved",
|
||||
"message": "כיוון אושר. ניתן להתחיל כתיבת טיוטה.",
|
||||
"direction": direction_doc,
|
||||
}, default=str, ensure_ascii=False, indent=2)
|
||||
return ok({"direction": direction_doc},
|
||||
message="כיוון אושר. ניתן להתחיל כתיבת טיוטה.")
|
||||
|
||||
|
||||
async def ingest_final_version(
|
||||
@@ -311,7 +308,7 @@ async def ingest_final_version(
|
||||
|
||||
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"])
|
||||
|
||||
@@ -321,12 +318,12 @@ async def ingest_final_version(
|
||||
final_text, _, _ = await extractor.extract_text(file_path)
|
||||
|
||||
if not final_text:
|
||||
return "לא סופק טקסט — יש לספק file_path או final_text."
|
||||
return err("לא סופק טקסט — יש לספק file_path או final_text.")
|
||||
|
||||
try:
|
||||
result = await learning_loop.process_final_version(case_id, final_text)
|
||||
except ValueError as e:
|
||||
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False, indent=2)
|
||||
return err(str(e))
|
||||
|
||||
# Auto-ingest into internal committee decisions corpus (best-effort).
|
||||
try:
|
||||
@@ -346,7 +343,7 @@ async def ingest_final_version(
|
||||
logger.warning("ingest_final_version: internal corpus ingestion failed (non-fatal): %s", e)
|
||||
result["internal_corpus_ingested"] = False
|
||||
|
||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
||||
return ok(result)
|
||||
|
||||
|
||||
# ── Chair feedback tools ──────────────────────────────────────────
|
||||
@@ -376,7 +373,7 @@ async def record_chair_feedback(
|
||||
"factual_error", "style", "other",
|
||||
]
|
||||
if category not in valid_categories:
|
||||
return f"קטגוריה לא חוקית. אפשרויות: {', '.join(valid_categories)}"
|
||||
return err(f"קטגוריה לא חוקית. אפשרויות: {', '.join(valid_categories)}")
|
||||
|
||||
feedback_id = await db.record_chair_feedback(
|
||||
case_id=case_id,
|
||||
@@ -386,15 +383,13 @@ async def record_chair_feedback(
|
||||
lesson_extracted=lesson_extracted,
|
||||
)
|
||||
|
||||
return json.dumps({
|
||||
"status": "ok",
|
||||
return ok({
|
||||
"feedback_id": str(feedback_id),
|
||||
"message": f"הערה נרשמה בהצלחה. קטגוריה: {category}.",
|
||||
"next_steps": [
|
||||
"כדי להפיק לקח מההערה, הפעל: analyze_chair_feedback",
|
||||
"כדי לסמן כמטופל: resolve_chair_feedback",
|
||||
],
|
||||
}, ensure_ascii=False, indent=2)
|
||||
}, message=f"הערה נרשמה בהצלחה. קטגוריה: {category}.")
|
||||
|
||||
|
||||
async def list_chair_feedback(
|
||||
@@ -425,7 +420,7 @@ async def list_chair_feedback(
|
||||
)
|
||||
|
||||
if not feedbacks:
|
||||
return "אין הערות שמתאימות לסינון."
|
||||
return empty("אין הערות שמתאימות לסינון.")
|
||||
|
||||
items = []
|
||||
for fb in feedbacks:
|
||||
@@ -440,7 +435,7 @@ async def list_chair_feedback(
|
||||
"date": fb["created_at"].isoformat() if fb.get("created_at") else None,
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
return ok({
|
||||
"total": len(items),
|
||||
"feedbacks": items,
|
||||
}, ensure_ascii=False, indent=2, default=str)
|
||||
})
|
||||
|
||||
@@ -234,14 +234,15 @@ def test_mcp_precedent_upload_rejects_arar_citation() -> None:
|
||||
"ARAR 8126-25 ב. קרן-נכסים",
|
||||
):
|
||||
result = loop.run_until_complete(call(citation))
|
||||
assert "error" in result, (
|
||||
# GAP-48: tools return the {status,data,message} envelope.
|
||||
assert result.get("status") == "error", (
|
||||
f"expected guard to reject {citation!r}, got {result!r}"
|
||||
)
|
||||
# The error message should mention internal_decision_upload so
|
||||
# the caller knows the alternative path.
|
||||
assert "internal_decision_upload" in result["error"], (
|
||||
assert "internal_decision_upload" in result["message"], (
|
||||
f"error message should redirect to internal_decision_upload, "
|
||||
f"got {result['error']!r}"
|
||||
f"got {result['message']!r}"
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
Reference in New Issue
Block a user