"""CONTENT/CONTRACTUAL-Pruefer / decision_method=LLM. present/absent ueber die LLM-Kaskade (`call_with_cascade`; prod: OVH-120b zuerst). Retrieval = GANZE Paragraph-Abschnitte zum Topic (nicht Top-k-Chunks — das war in der AGB-Validierung der Schluessel). KEIN DEFECT — Korrektheits-/Defekt-Pruefung ist ein separater Modus. present=None bei Fehler (fail-safe: Aufrufer behaelt Keyword-Ergebnis). (Validiert an AGB delivery/warranty.) """ from __future__ import annotations import json import logging import re from .base import CheckResult, ControlSpec, DocContext, VerificationMethod logger = logging.getLogger(__name__) _SECTION = re.compile(r"(?m)(?=^\s*(?:§\s*)?\d+[\.\)]\s)") _SYS = ( "Du bist deutscher Compliance-Rechtsexperte. Entscheide, ob die genannte " "Pflicht in den vorgelegten Abschnitten vorhanden ist. NUR die Abschnitte " 'zaehlen. Antworte NUR JSON: {"verdict":"ERFUELLT|FEHLT","zitat":"woertlich ' 'oder leer","begruendung":"1 Satz"}.' ) def _sections(text: str) -> list[str]: return [s.strip() for s in _SECTION.split(text) if s.strip()] def _parse(txt: str) -> dict: out = (txt or "").strip() if out.startswith("```"): out = out.split("```", 2)[1] out = out[4:] if out.startswith("json") else out a, b = out.find("{"), out.rfind("}") return json.loads(out[a:b + 1] if 0 <= a < b else out) class LLMChecker: verification_method = VerificationMethod.CONTENT async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult: text = doc.text or "" if len(text) < 50: return CheckResult(present=None, source="llm") secs = _sections(text) if ctrl.topic_regex: rel = [s for s in secs if re.search(ctrl.topic_regex, s, re.I)][:6] or secs[:6] else: rel = secs[:6] question = ctrl.question or f"Ist die Pflicht '{ctrl.label}' im Text vorhanden?" try: from compliance.services.llm_cascade import call_with_cascade r = await call_with_cascade( _SYS, json.dumps({"frage": question, "abschnitte": rel}, ensure_ascii=False), min_confidence=0.6, max_tokens=500, ) obj = _parse(r.get("text")) verdict = obj.get("verdict") zitat = (obj.get("zitat") or "")[:120] if verdict not in ("ERFUELLT", "FEHLT"): return CheckResult(present=None, evidence=zitat, source=r.get("source", "?")) return CheckResult( present=verdict == "ERFUELLT", evidence=zitat, confidence=float(r.get("confidence") or 0.0), source=r.get("source", "llm"), ) except Exception as e: logger.info("llm checker fail %s: %s", ctrl.control_id, str(e)[:80]) return CheckResult(present=None, source="error")