Chaudhry Waleed
Deploy RICS v2 (senior baseline, CPU Spaces Dockerfile)
2a6c3b9
Raw
History Blame Contribute Delete
5.35 kB
"""Approach 2: per-section LLM coverage judge (notes vs generated prose)."""
from __future__ import annotations
import logging
from backend.evaluation.judge_llm import (
call_judge_parse,
is_available,
resolved_max_tokens,
resolved_model,
resolved_provider,
resolved_reasoning_effort,
unavailable_reason,
)
from backend.evaluation.models import (
CoverageJudgeResponse,
NoteFactJudgment,
SectionEvalInput,
SectionEvaluation,
)
from backend.evaluation.prompts import build_coverage_messages, prompt_record
logger = logging.getLogger(__name__)
_VALID_STATUS = frozenset({"covered", "missing", "partial"})
def _normalize_judgments(
observations: list[str],
raw: CoverageJudgeResponse,
) -> tuple[list[NoteFactJudgment], list[str], int]:
"""Map structured judge output onto the input notes; score omitted notes missing."""
by_note: dict[str, NoteFactJudgment] = {}
for item in raw.judgments:
note = (item.note or "").strip()
status = (item.status or "").strip().lower()
if not note or status not in _VALID_STATUS:
continue
by_note[note.lower()] = NoteFactJudgment(
note=note,
status=status, # type: ignore[arg-type]
evidence=(item.evidence or "").strip(),
)
missing_listed = {
str(f).strip().lower() for f in (raw.missing_facts or []) if str(f).strip()
}
judgments: list[NoteFactJudgment] = []
omitted = 0
for obs in observations:
key = obs.strip()
if not key:
continue
existing = by_note.get(key.lower())
if existing is not None:
judgments.append(existing)
continue
omitted += 1
judgments.append(
NoteFactJudgment(
note=key,
status="missing",
evidence="omitted_by_judge" if key.lower() not in missing_listed else "",
)
)
missing_facts = [j.note for j in judgments if j.status in ("missing", "partial")]
return judgments, missing_facts, omitted
def _rollup_coverage(
judgments: list[NoteFactJudgment],
) -> tuple[int, int, int, float | None]:
covered = sum(1 for j in judgments if j.status == "covered")
missing = sum(1 for j in judgments if j.status == "missing")
partial = sum(1 for j in judgments if j.status == "partial")
total = len(judgments)
rate = (covered + 0.5 * partial) / total if total else None
return covered, missing, partial, rate
def _coverage_prompt_payload(messages: list[dict[str, str]]) -> dict:
effort = resolved_reasoning_effort()
return {
"coverage": prompt_record(
messages,
model=resolved_model(),
reasoning_effort=effort,
max_tokens=resolved_max_tokens(effort),
provider=resolved_provider(),
)
}
async def score_section_coverage(inp: SectionEvalInput) -> SectionEvaluation:
"""Run Approach 2 coverage judge for one section."""
observations = [o.strip() for o in (inp.observations or []) if o and o.strip()]
base = SectionEvaluation(
section_id=inp.section_id,
title=inp.title,
observations=observations,
generated_text=inp.generated_text or "",
baseline_text=inp.baseline_text or "",
)
if not observations:
return base
if not (inp.generated_text or "").strip():
judgments = [
NoteFactJudgment(note=o, status="missing", evidence="empty generated text")
for o in observations
]
covered, missing, partial, rate = _rollup_coverage(judgments)
base.note_judgments = judgments
base.covered_count = covered
base.missing_count = missing
base.partial_count = partial
base.coverage_rate = rate
base.missing_facts = [j.note for j in judgments]
return base
if not is_available():
base.error = unavailable_reason()
return base
messages = build_coverage_messages(
section_id=inp.section_id,
title=inp.title,
observations=observations,
generated_text=inp.generated_text,
)
base.prompt = _coverage_prompt_payload(messages)
try:
parsed = await call_judge_parse(
messages,
response_format=CoverageJudgeResponse,
call_label="evaluation_coverage",
section_id=inp.section_id,
)
except Exception as exc: # noqa: BLE001
logger.warning(
"evaluation_coverage_failed section=%s err=%s",
inp.section_id,
exc,
)
base.error = str(exc)
return base
if parsed is None:
base.error = "empty_judge_response"
return base
if not parsed.judgments:
base.error = "empty_judge_response"
return base
judgments, missing_facts, omitted = _normalize_judgments(observations, parsed)
if omitted:
base.error = f"judge_omitted_{omitted}_of_{len(observations)}_notes"
covered, missing, partial, rate = _rollup_coverage(judgments)
base.note_judgments = judgments
base.covered_count = covered
base.missing_count = missing
base.partial_count = partial
base.coverage_rate = rate
base.missing_facts = missing_facts
return base