diff --git a/mcp-server/src/legal_mcp/config.py b/mcp-server/src/legal_mcp/config.py index c2e8c4b..c930a1f 100644 --- a/mcp-server/src/legal_mcp/config.py +++ b/mcp-server/src/legal_mcp/config.py @@ -54,6 +54,8 @@ REDIS_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6380/0") # pinned. HALACHA_EXTRACT_MODEL = os.environ.get("HALACHA_EXTRACT_MODEL", "claude-opus-4-8") HALACHA_EXTRACT_EFFORT = os.environ.get("HALACHA_EXTRACT_EFFORT", "xhigh") +HALACHA_CORROBORATION_MATCH_FLOOR = float(os.environ.get("HALACHA_CORROBORATION_MATCH_FLOOR", "0.50")) +HALACHA_CORROBORATION_MIN_CITES = int(os.environ.get("HALACHA_CORROBORATION_MIN_CITES", "2")) # Voyage AI VOYAGE_API_KEY = os.environ.get("VOYAGE_API_KEY", "") diff --git a/mcp-server/src/legal_mcp/services/corroboration.py b/mcp-server/src/legal_mcp/services/corroboration.py index 78ad88a..089d68b 100644 --- a/mcp-server/src/legal_mcp/services/corroboration.py +++ b/mcp-server/src/legal_mcp/services/corroboration.py @@ -25,6 +25,14 @@ def _coerce_treatment(raw: dict) -> str: return t if t in _VALID_TREATMENT else "mentioned" +def accept_match(best: tuple[str, float] | None, floor: float = config.HALACHA_CORROBORATION_MATCH_FLOOR) -> str | None: + """Return the halacha_id iff similarity clears the floor (INV-COR3).""" + if not best: + return None + halacha_id, sim = best + return halacha_id if sim >= floor else None + + _TREATMENT_PROMPT = """אתה משפטן בכיר. נתון ציטוט של פסק/החלטה קודמים בתוך החלטה מאוחרת. סווג כיצד ההחלטה המאוחרת **מטפלת** בתקדים המצוטט, לפי אחת מהקטגוריות: - followed — אימצה והחילה את ההלכה. diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index b2222a3..1c4150d 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -3386,6 +3386,18 @@ async def update_halacha( return dict(row) if row else None +async def nearest_halacha_for_vector(case_law_id: UUID, vec: list[float]) -> tuple[str, float] | None: + """Best-matching halacha of `case_law_id` for a context embedding (cosine).""" + pool = await get_pool() + row = await pool.fetchrow( + "SELECT id::text AS id, 1 - (embedding <=> $2) AS sim " + "FROM halachot WHERE case_law_id = $1 AND embedding IS NOT NULL " + "ORDER BY embedding <=> $2 LIMIT 1", + case_law_id, vec, + ) + return (row["id"], float(row["sim"])) if row else None + + async def search_precedent_library_semantic( query_embedding: list[float], practice_area: str = "", diff --git a/mcp-server/tests/test_corroboration.py b/mcp-server/tests/test_corroboration.py index 07216e7..0aff981 100644 --- a/mcp-server/tests/test_corroboration.py +++ b/mcp-server/tests/test_corroboration.py @@ -15,3 +15,12 @@ def test_treatment_polarity(): assert cor.is_positive("followed") and cor.is_positive("explained") assert cor.is_negative("distinguished") and cor.is_negative("overruled") assert not cor.is_positive("mentioned") and not cor.is_negative("mentioned") + +def test_match_accepts_above_threshold(): + assert cor.accept_match(("h1", 0.62), floor=0.50) == "h1" + +def test_match_rejects_below_threshold(): + assert cor.accept_match(("h1", 0.41), floor=0.50) is None + +def test_match_rejects_empty(): + assert cor.accept_match(None, floor=0.50) is None