RISHIVEL's picture
space deploy: fix zerogpu pickling
087643a
Raw History Blame Contribute Delete
3.78 kB
# Decides if the evidence is good enough to answer confidently - before the
# LLM writes anything. Kept as a separate deterministic check because asking
# a model "are you confident?" just gets more text, not a real signal - it's
# equally fluent at a confident-sounding guess as an honest refusal.
from __future__ import annotations
from dataclasses import dataclass, field
from app.reasoning.evidence import Evidence
# Below this, a single detection isn't trustworthy enough to answer from.
# 0.50 rather than something stricter like 0.7 - correctly localised
# regions on this dataset tend to score well above 0.6 in my own runs.
CONFIDENCE_FLOOR = 0.50
# For counting: don't report an exact number if too much of it sits in this
# shaky band. Below 0.25 is just noise; 0.25-0.50 is "plausible but I
# wouldn't bet on the count."
AMBIGUOUS_BAND = (0.25, CONFIDENCE_FLOOR)
AMBIGUOUS_BAND_FRACTION = 0.30
@dataclass
class GuardrailVerdict:
triggered: bool
rule: str | None = None
detail: dict = field(default_factory=dict)
def _confidences_for(evidence: Evidence, target_classes: list[str]) -> list[float]:
"""Max confidence per target class, ignoring unrelated classes on the page."""
values: list[float] = []
for class_name in target_classes:
stats = evidence.confidence_stats.get(class_name)
if stats is None:
continue
values.append(stats.max)
return values
def evaluate_guardrail(
evidence: Evidence,
target_classes: list[str],
task_type: str,
saturated: bool = False,
) -> GuardrailVerdict:
"""Four rules, checked in order, most specific first - some can overlap
and I want the more useful explanation to win (e.g. "found nothing"
beats "page was saturated" even when both are technically true)."""
raw_confidences = [
value
for class_name in target_classes
for value in evidence.confidences_by_class.get(class_name, [])
]
target_counts = {
class_name: evidence.class_counts.get(class_name, 0)
for class_name in target_classes
}
total_target_detections = sum(target_counts.values())
# nothing of the target class found at all
if total_target_detections == 0:
return GuardrailVerdict(
triggered=True,
rule="no_detections",
detail={"target_classes": target_classes},
)
max_confidences = _confidences_for(evidence, target_classes)
observed_max = max(max_confidences) if max_confidences else 0.0
# found something, but nothing clears the confidence floor
if observed_max < CONFIDENCE_FLOOR:
return GuardrailVerdict(
triggered=True,
rule="all_below_tau",
detail={"tau": CONFIDENCE_FLOOR, "observed_max": observed_max},
)
# counting only: too much of the count sits in the shaky band.
# doesn't apply to "is there one?" - one confident hit is enough for that.
if task_type == "count" and raw_confidences:
low, high = AMBIGUOUS_BAND
in_band = sum(1 for c in raw_confidences if low <= c < high)
if raw_confidences and (in_band / len(raw_confidences)) > AMBIGUOUS_BAND_FRACTION:
return GuardrailVerdict(
triggered=True,
rule="ambiguous_band",
detail={
"band": list(AMBIGUOUS_BAND),
"fraction_in_band": round(in_band / len(raw_confidences), 2),
},
)
# page had more regions than RT-DETR's query budget could emit -
# a count here is unreliable regardless of confidence
if saturated:
return GuardrailVerdict(triggered=True, rule="query_saturated", detail={})
return GuardrailVerdict(triggered=False)