feat(X11): citation-corroboration Phase 1 — the signal (no approval change) #27
52
mcp-server/src/legal_mcp/services/corroboration.py
Normal file
52
mcp-server/src/legal_mcp/services/corroboration.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""X11 citation corroboration — classify treatment, match to halacha, aggregate.
|
||||||
|
|
||||||
|
Phase 1: builds the SIGNAL only (no approval changes). See docs/spec/X11.
|
||||||
|
All LLM calls go through the local claude_session bridge (Opus 4.8 @ xhigh),
|
||||||
|
same architectural rule as the other extractors (local MCP only).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from legal_mcp import config
|
||||||
|
from legal_mcp.config import parse_llm_json
|
||||||
|
from legal_mcp.services import claude_session
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TREATMENT_POSITIVE = {"followed", "explained"}
|
||||||
|
TREATMENT_NEGATIVE = {"distinguished", "criticized", "questioned", "overruled"}
|
||||||
|
TREATMENT_NEUTRAL = {"mentioned"}
|
||||||
|
_VALID_TREATMENT = TREATMENT_POSITIVE | TREATMENT_NEGATIVE | TREATMENT_NEUTRAL
|
||||||
|
|
||||||
|
def is_positive(t: str) -> bool: return t in TREATMENT_POSITIVE
|
||||||
|
def is_negative(t: str) -> bool: return t in TREATMENT_NEGATIVE
|
||||||
|
|
||||||
|
def _coerce_treatment(raw: dict) -> str:
|
||||||
|
t = str((raw or {}).get("treatment", "")).strip().lower()
|
||||||
|
return t if t in _VALID_TREATMENT else "mentioned"
|
||||||
|
|
||||||
|
|
||||||
|
_TREATMENT_PROMPT = """אתה משפטן בכיר. נתון ציטוט של פסק/החלטה קודמים בתוך החלטה מאוחרת.
|
||||||
|
סווג כיצד ההחלטה המאוחרת **מטפלת** בתקדים המצוטט, לפי אחת מהקטגוריות:
|
||||||
|
- followed — אימצה והחילה את ההלכה.
|
||||||
|
- explained — הסבירה/הזכירה בלי לחלוק.
|
||||||
|
- distinguished — אבחנה (קבעה שלא חל בנסיבות).
|
||||||
|
- criticized — מתחה ביקורת בלי לבטל.
|
||||||
|
- questioned — הטילה ספק.
|
||||||
|
- overruled — דחתה/ביטלה את ההלכה.
|
||||||
|
- mentioned — אזכור-אגב בלי טיפול.
|
||||||
|
החזר JSON בלבד: {"treatment": "<קטגוריה>"}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def classify_treatment(cited_citation: str, context: str) -> str:
|
||||||
|
"""Return one treatment label for how `context` treats `cited_citation`."""
|
||||||
|
user = f"תקדים מצוטט: {cited_citation}\n\n--- ההקשר המצטט ---\n{context}\n--- סוף ---"
|
||||||
|
try:
|
||||||
|
result = await claude_session.query_json(
|
||||||
|
user, system=_TREATMENT_PROMPT,
|
||||||
|
model=config.HALACHA_EXTRACT_MODEL or None,
|
||||||
|
effort=config.HALACHA_EXTRACT_EFFORT or None,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("classify_treatment failed: %s", e)
|
||||||
|
return "mentioned"
|
||||||
|
return _coerce_treatment(result if isinstance(result, dict) else {})
|
||||||
17
mcp-server/tests/test_corroboration.py
Normal file
17
mcp-server/tests/test_corroboration.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import pytest
|
||||||
|
from legal_mcp.services import corroboration as cor
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("raw,expected", [
|
||||||
|
({"treatment": "followed"}, "followed"),
|
||||||
|
({"treatment": "OVERRULED"}, "overruled"), # case-insensitive
|
||||||
|
({"treatment": "bananas"}, "mentioned"), # unknown -> neutral default
|
||||||
|
({}, "mentioned"), # missing -> neutral default
|
||||||
|
])
|
||||||
|
def test_coerce_treatment(raw, expected):
|
||||||
|
assert cor._coerce_treatment(raw) == expected
|
||||||
|
|
||||||
|
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")
|
||||||
Reference in New Issue
Block a user