SentinelAI / app /rag /quality_evaluator.py
NOT-OMEGA's picture
Upload 40 files
6675588 verified
Raw History Blame Contribute Delete
4.66 kB
"""
quality_evaluator.py — Production Pre-Generation Retrieval Quality & Confidence Evaluator.
Measures context relevance, factual grounding density, and lexical-semantic alignment
before passing contexts to the LLM to prevent hallucinations.
"""
import re
from typing import List, Dict, Optional
from dataclasses import dataclass, field
import logging
from .pdf_chunker import DocumentChunk
logger = logging.getLogger(__name__)
@dataclass
class RetrievalQualityReport:
"""Detailed quality evaluation of the retrieved contexts."""
quality_score: float # 0.0 to 1.0 (0% to 100%)
confidence_level: str # 'high', 'medium', 'low'
is_sufficient: bool
context_relevance: float
information_density: float
matched_key_concepts: List[str]
missing_concepts: List[str]
recommended_action: str # 'generate', 'clarify', 'fallback'
class RetrievalQualityEvaluator:
"""
Evaluates whether the retrieved Top-K chunks contain sufficient evidence
to accurately and truthfully answer the user's query before invoking the LLM.
"""
POLICY_KEYWORDS = {
"refund", "rate limit", "chargeback", "webhook", "tokenization", "settlement",
"kyb", "uptime", "subscription", "pricing", "sla", "reversal", "quota",
"dispute", "dlq", "rsa", "kms", "swift", "currencies", "mandate", "dunning"
}
def __init__(self, sufficiency_threshold: float = 0.35):
self.sufficiency_threshold = sufficiency_threshold
def evaluate(
self,
query: str,
retrieved_chunks: List[DocumentChunk],
scores: Optional[List[float]] = None,
) -> RetrievalQualityReport:
"""
Evaluate retrieval quality before trusting the context for LLM synthesis.
"""
if not retrieved_chunks:
return RetrievalQualityReport(
quality_score=0.0,
confidence_level="low",
is_sufficient=False,
context_relevance=0.0,
information_density=0.0,
matched_key_concepts=[],
missing_concepts=list(re.findall(r"\w{3,}", query.lower())),
recommended_action="fallback",
)
lower_query = query.lower()
query_terms = set(re.findall(r"\b[A-Za-z0-9\-\.]{3,}\b", lower_query))
# Filter out common filler words
filler = {"what", "when", "where", "which", "how", "does", "have", "with", "from", "tell", "explain", "about"}
salient_query_terms = query_terms - filler
# Combine text of all retrieved chunks
combined_text = " ".join([c.text.lower() + " " + (c.section_title or "").lower() for c in retrieved_chunks])
# 1. Calculate Concept Coverage
matched_concepts = []
missing_concepts = []
for term in salient_query_terms:
if term in combined_text:
matched_concepts.append(term)
else:
missing_concepts.append(term)
if salient_query_terms:
relevance = len(matched_concepts) / len(salient_query_terms)
else:
relevance = 0.85 # Fallback for very short greetings/general queries
# 2. Information Density (ratio of numbers, acronyms, and uppercase anchors)
numbers_and_codes = re.findall(r"\b(?:[A-Z]{2,}-\d+|\d+(?:\.\d+)?%?|\$\d+)\b", combined_text)
density = min(1.0, len(numbers_and_codes) / 8.0)
# 3. Base similarity component
top_score = scores[0] if scores and len(scores) > 0 else 0.5
top_score_norm = max(0.0, min(1.0, top_score))
# Composite Retrieval Quality Score
composite_quality = (
(relevance * 0.50) +
(top_score_norm * 0.30) +
(density * 0.20)
)
composite_quality = round(min(1.0, max(0.0, composite_quality)), 3)
if composite_quality >= 0.70:
confidence = "high"
action = "generate"
elif composite_quality >= self.sufficiency_threshold:
confidence = "medium"
action = "generate"
else:
confidence = "low"
action = "clarify"
is_sufficient = composite_quality >= self.sufficiency_threshold
return RetrievalQualityReport(
quality_score=composite_quality,
confidence_level=confidence,
is_sufficient=is_sufficient,
context_relevance=round(relevance, 3),
information_density=round(density, 3),
matched_key_concepts=matched_concepts,
missing_concepts=missing_concepts,
recommended_action=action,
)