Datavision / backend /core /answer_evaluator.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
ee7d7b9
Raw
History Blame Contribute Delete
7.86 kB
"""
Answer Evaluator - Check if AI response is grounded in data
============================================================
Simple evaluation to ensure answers are based on provided context.
Returns confidence score and flags unverified claims.
"""
import re
from typing import Dict, List, Tuple
def extract_numbers(text: str) -> List[str]:
"""Extract all numbers (including currency) from text"""
# Match numbers with optional currency symbols and commas
patterns = [
r'[\$€£₹]?\s*[\d,]+\.?\d*\s*(?:million|billion|M|B|K)?',
r'\d+\.?\d*\s*%', # Percentages
r'\d+', # Plain numbers
]
numbers = []
for pattern in patterns:
matches = re.findall(pattern, text, re.IGNORECASE)
numbers.extend([m.strip() for m in matches if m.strip()])
return list(set(numbers))
def extract_entities(text: str) -> List[str]:
"""Extract key entities (names, terms) from text"""
# Remove common stop words and extract significant terms
stop_words = {'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will',
'would', 'could', 'should', 'may', 'might', 'must', 'shall',
'can', 'need', 'dare', 'ought', 'used', 'to', 'of', 'in',
'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into',
'through', 'during', 'before', 'after', 'above', 'below',
'between', 'under', 'again', 'further', 'then', 'once',
'here', 'there', 'when', 'where', 'why', 'how', 'all',
'each', 'few', 'more', 'most', 'other', 'some', 'such',
'no', 'nor', 'not', 'only', 'own', 'same', 'than', 'too',
'very', 'just', 'and', 'but', 'or', 'if', 'because',
'this', 'that', 'these', 'those', 'it', 'its'}
# Extract words that look like entities (capitalized or specific patterns)
words = re.findall(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b', text)
# Also get quoted terms
quoted = re.findall(r'"([^"]+)"', text)
# Filter and combine
entities = []
for word in words + quoted:
if word.lower() not in stop_words and len(word) > 2:
entities.append(word)
return list(set(entities))
def normalize_number(num_str: str) -> float:
"""Convert number string to float for comparison"""
try:
# Remove currency symbols and commas
cleaned = re.sub(r'[\$€£₹,\s]', '', num_str)
# Handle M/K/B suffixes
multipliers = {'k': 1000, 'm': 1000000, 'b': 1000000000,
'million': 1000000, 'billion': 1000000000}
for suffix, mult in multipliers.items():
if cleaned.lower().endswith(suffix):
cleaned = cleaned[:-len(suffix)]
return float(cleaned) * mult
return float(cleaned)
except:
return None
def evaluate_answer(answer: str, context: str, query: str = "") -> Dict:
"""
Evaluate if answer is grounded in the provided context.
Args:
answer: The AI-generated response
context: The retrieved context/data
query: Original user query (optional)
Returns:
{
"grounded": bool,
"confidence": int (0-100),
"verified_claims": [...],
"unverified_claims": [...],
"warning": str or None
}
"""
if not answer or not context:
return {
"grounded": False,
"confidence": 0,
"verified_claims": [],
"unverified_claims": [],
"warning": "Missing answer or context"
}
context_lower = context.lower()
answer_lower = answer.lower()
# ⚡ FIX: Truncate context to prevent catastrophic regex CPU hanging on massive datasets
max_context_len = 5000
safe_context = context if len(context) < max_context_len else context[:max_context_len]
# Extract numbers from answer
answer_numbers = extract_numbers(answer)
context_numbers = extract_numbers(safe_context)
# Extract entities from answer
answer_entities = extract_entities(answer)
verified_claims = []
unverified_claims = []
# Check numbers
for num in answer_numbers:
num_normalized = normalize_number(num)
found = False
# Direct string match
if num.lower() in context_lower:
found = True
# Normalized value match
if not found and num_normalized:
for ctx_num in context_numbers:
ctx_normalized = normalize_number(ctx_num)
if ctx_normalized and abs(num_normalized - ctx_normalized) < 0.01:
found = True
break
if found:
verified_claims.append(f"Number: {num}")
else:
unverified_claims.append(f"Number: {num}")
# Check entities
for entity in answer_entities:
if entity.lower() in context_lower:
verified_claims.append(f"Entity: {entity}")
else:
# Partial match
entity_words = entity.lower().split()
if any(word in context_lower for word in entity_words if len(word) > 3):
verified_claims.append(f"Entity (partial): {entity}")
else:
unverified_claims.append(f"Entity: {entity}")
# Calculate confidence
total_claims = len(verified_claims) + len(unverified_claims)
if total_claims == 0:
confidence = 70 # Neutral if no claims to verify
else:
confidence = int((len(verified_claims) / total_claims) * 100)
# Determine grounding
grounded = confidence >= 50
# Generate warning if needed
warning = None
if len(unverified_claims) > 0:
if confidence < 30:
warning = "⚠️ Low confidence: Many claims could not be verified in the data."
elif confidence < 60:
warning = "⚠️ Some claims may not be directly supported by the data."
return {
"grounded": grounded,
"confidence": confidence,
"verified_claims": verified_claims[:5], # Limit for display
"unverified_claims": unverified_claims[:5],
"warning": warning
}
def get_confidence_badge(confidence: int) -> str:
"""Return emoji badge based on confidence level"""
if confidence >= 90:
return "🟢 High Confidence"
elif confidence >= 70:
return "🟡 Medium Confidence"
elif confidence >= 50:
return "🟠 Low Confidence"
else:
return "🔴 Very Low Confidence"
# Test
if __name__ == "__main__":
context = """
Revenue data from hr_dataset_500_rows.csv:
- Total employees: 500
- Average salary: $75,000
- Department: Engineering, Sales, Marketing, HR
- Performance ratings: 1-5 scale
"""
# Good answer (grounded)
good_answer = "There are 500 employees with an average salary of $75,000 across Engineering, Sales, and Marketing departments."
# Bad answer (hallucinated)
bad_answer = "The company has $50 million in revenue and 10,000 customers across 50 countries."
print("=== Good Answer ===")
result = evaluate_answer(good_answer, context)
print(f"Confidence: {result['confidence']}%")
print(f"Grounded: {result['grounded']}")
print(f"Verified: {result['verified_claims']}")
print(f"Unverified: {result['unverified_claims']}")
print("\n=== Bad Answer ===")
result = evaluate_answer(bad_answer, context)
print(f"Confidence: {result['confidence']}%")
print(f"Grounded: {result['grounded']}")
print(f"Warning: {result['warning']}")