""" 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']}")