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:
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user