feat(mcp): FU-14 GAP-48 פרוסה 1 — envelope אחיד (SSoT) + משפחת-חיפוש

INV-TOOL1: כלי-ה-MCP החזירו 3 מוסכמות סותרות (raw payload / {error} /
{status,message} אד-הוק) + 5 עותקי _ok/_err משוכפלים. נוצר tools/envelope.py
כמקור-אמת יחיד: ok/empty/err → {status,data,message}, כש-status מבחין
מפורשות הצלחה/ריק/שגיאה.

פרוסה 1 ממירה את משפחת-החיפוש (search_decisions, search_case_documents,
find_similar_cases, search_internal_decisions). web/app.py מפרק את המעטפת
דרך envelope_unwrap כדי לשמר את חוזה-ה-UI↔API (X6) ללא-שינוי — תשובת ה-HTTP
זהה (list על hits, {"message"} על ריק/שגיאה). טסט test_search_domain_scope
עודכן לחוזה החדש (5/5 עוברים).

החלטה: הדרגתי לפי-משפחה ולא big-bang. מפת-צרכנים: server.py pass-through,
web-ui מבודד (/api/*), רק 17 כלים נצרכים ישירות מ-app.py → סיכון מינימלי
לסוכנים החיים. ~73 כלים נותרו לפרוסות הבאות.

Invariants: מקדם INV-TOOL1 (envelope עקבי) + G2 (SSoT, ביטול כפילות _ok/_err).
לא נוגע ב-G1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 16:32:07 +00:00
parent c52b5986a3
commit aa0a736a7b
6 changed files with 92 additions and 26 deletions

View File

@@ -0,0 +1,58 @@
"""מעטפת-תשובה קנונית לכלי-ה-MCP (GAP-48 / INV-TOOL1).
מקור-אמת יחיד לצורת-הפלט של כל כלי. כל כלי שעבר מיגרציה מחזיר מחרוזת-JSON
אחידה ``{status, data, message}`` — לא list-לפעמים, string-לפעמים, ``{error}``
לפעמים. שלושת המצבים מובחנים מפורשות (INV-TOOL1):
status — "ok" הצלחה עם payload ב-``data``.
"empty" שאילתה תקינה שלא החזירה תוצאות (מובחן מ-error).
"error" הקריאה נכשלה; ``message`` מסביר.
data — ה-payload (list/dict/scalar) או null.
message — הערה קריאה-לאדם (עברית), בעיקר ל-empty/error.
מחליף את ה-``_ok``/``_err`` שהשתכפלו ב-5 קבצי-כלים עם 3 מוסכמות שונות (G2).
צרכן-API ב-``web/`` שמעביר פלט-כלי ל-HTTP חייב **לפרק** את המעטפת
(להחזיר ``data``) כדי לשמר את חוזה-ה-UI↔API (X6) — ראה ``envelope_unwrap``.
"""
from __future__ import annotations
import json
from typing import Any
def _dump(obj: dict) -> str:
return json.dumps(obj, ensure_ascii=False, indent=2, default=str)
def ok(data: Any = None, message: str = "") -> str:
"""הצלחה עם payload."""
return _dump({"status": "ok", "data": data, "message": message})
def empty(message: str = "", data: Any = None) -> str:
"""שאילתה תקינה ללא תוצאות — מובחן מהצלחה (data לא-ריק) ומשגיאה."""
return _dump({"status": "empty", "data": [] if data is None else data, "message": message})
def err(message: str, *, data: Any = None) -> str:
"""כשל בקריאה."""
return _dump({"status": "error", "data": data, "message": message})
def envelope_unwrap(parsed: Any) -> Any:
"""פירוק מעטפת לצורך צרכן-API שמשמר חוזה-HTTP ישן.
בהינתן dict-מעטפת מפורסר (``json.loads`` של פלט-כלי שעבר מיגרציה):
status=="ok" → מחזיר ``data`` (ה-payload המקורי, למשל list).
status in {empty,error}→ מחזיר ``{"message": message}`` (תאימות-לאחור
לצורה שצרכני-ה-API החזירו על מחרוזת-פרוזה).
קלט שאינו מעטפת (אין מפתח ``status``) מוחזר כמות-שהוא — בטוח לתקופת-המעבר
שבה רק חלק מהכלים עברו מיגרציה.
"""
if isinstance(parsed, dict) and "status" in parsed and "data" in parsed:
if parsed["status"] == "ok":
return parsed["data"]
return {"message": parsed.get("message", "")}
return parsed