feat(corroboration): treatment classifier + polarity (INV-COR2, X11)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
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