Spaces:
Running
Running
File size: 7,858 Bytes
ee7d7b9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | """
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']}")
|