File size: 11,092 Bytes
f07e656 c89cad9 f07e656 c89cad9 114f82f f07e656 c89cad9 f07e656 c89cad9 f07e656 114f82f f07e656 114f82f c89cad9 f07e656 c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f f07e656 114f82f f07e656 c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f f07e656 114f82f c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f c89cad9 f07e656 114f82f c89cad9 114f82f c89cad9 114f82f c89cad9 f07e656 114f82f c89cad9 f07e656 c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f f07e656 114f82f f07e656 c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f c89cad9 114f82f f07e656 c89cad9 f07e656 114f82f c89cad9 114f82f c89cad9 f07e656 c89cad9 114f82f c89cad9 f07e656 c89cad9 114f82f c89cad9 114f82f f07e656 c89cad9 114f82f f07e656 c89cad9 f07e656 c89cad9 | 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 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | import re
import numpy as np
from typing import Dict, List, Optional, Tuple
import json
from collections import defaultdict
import spacy
from transformers import pipeline
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import hashlib
from datetime import datetime
import concurrent.futures
import random
class AetherScoreEvaluator:
def __init__(self):
# NLP models
try:
self.nlp = spacy.load("en_core_web_sm")
except OSError:
print("Downloading 'en_core_web_sm' spacy model...")
spacy.cli.download("en_core_web_sm")
self.nlp = spacy.load("en_core_web_sm")
# LLM Judge Model
self.judge_model = pipeline(
"text2text-generation",
model="google/flan-t5-small",
device=-1 # 0 for GPU
)
# Sentence Transformer for Sentence ----> Embedding
self.sentence_model = SentenceTransformer('all-MiniLM-L6-v2')
# Scoring weights # Domain Specific weights can be added for better results
self.weights = {'instruction_following': 0.25, 'hallucination_score': 0.20,
'assumption_control': 0.20, 'coherence': 0.20, 'accuracy': 0.15}
# In-memory cache
self.cache = {}
# LLM Judge
def _evaluate_with_llm_judge(self, prompt: str, response: str) -> Dict:
print("Using HF model LLM Judge...")
# Building Query for LLM Judge
query = (
f"Prompt: {prompt}\n"
f"Response: {response}\n\n"
"Evaluate the response and provide:\n"
"hallucination_score: float between 0 and 1 (1 = fully grounded, 0 = full hallucination)\n"
"assumption_control: float between 0 and 1 (1 = no assumptions, 0 = many unsupported assumptions)\n"
"Then explain briefly in one or two sentences."
)
# Calling our LLM
try:
result = self.judge_model(query, max_new_tokens=128, truncation=True)
output = result[0]['generated_text']
except Exception as e:
return {
"hallucination_score": (0.0, f"HF model failed: {e}"),
"assumption_control": (0.0, f"HF model failed: {e}")
}
# Parsing scores from model output
halluc_score = 0.0
assumption_score = 0.0
explanation = output
match_h = re.search(r"hallucination[_ ]?score[: ]?([01](?:\.\d+)?)", output, re.I)
match_a = re.search(r"assumption[_ ]?control[: ]?([01](?:\.\d+)?)", output, re.I)
if match_h:
halluc_score = float(match_h.group(1))
if match_a:
assumption_score = float(match_a.group(1))
return {
"hallucination_score": (halluc_score, explanation),
"assumption_control": (assumption_score, explanation)
}
# Single Evaluation # Inputs-->> Prompt, Agent Response, Expected Answer(Optional), Agent Name and Task type( General, QA, Summarizaton)etc
def evaluate_single(self, prompt: str, response: str, expected_answer: Optional[str] = None, task_type: str = "general") -> Dict:
# Generating Eval ID
eval_id = self._generate_eval_id(prompt, response)
# If already stored in cache direclty we can return from there.
if eval_id in self.cache:
return self.cache[eval_id]
scores, reasons = {}, {}
# Taking Back scores and reasons of hallucination and assumption control from LLM Judge
llm_judge_results = self._evaluate_with_llm_judge(prompt, response)
scores['hallucination_score'], reasons['hallucination_score'] = llm_judge_results['hallucination_score']
scores['assumption_control'], reasons['assumption_control'] = llm_judge_results['assumption_control']
# Evaluating Instruction Following, Coherence and Accuracy
scores['instruction_following'], reasons['instruction_following'] = self._evaluate_instruction_following(prompt, response)
scores['coherence'], reasons['coherence'] = self._evaluate_coherence(response)
scores['accuracy'], reasons['accuracy'] = self._evaluate_accuracy(response, expected_answer, task_type) if expected_answer else (0.5, "No expected answer provided.")
# Calculating Overall Score
scores['overall_score'] = self._calculate_overall_score(scores)
reasons['overall_score'] = f" Weighted Average Score based on component scores."
# Updating Eval ID, Timestamp and Task Type in Scores
scores.update({'eval_id': eval_id, 'timestamp': datetime.now().isoformat(), 'task_type': task_type})
# Updating scores(Eval ID, timestamp and task_type) and reasons(all scores) in result
result = {"scores": scores, "reasons": reasons}
#Storing results with corresponding Eval ID in cache
self.cache[eval_id] = result
return result
# Batch Evaluation # Input of JSON/CSV file
def evaluate_batch(self, data: List[Dict], mode: str = "comprehensive") -> List[Dict]:
"""Process a batch of evaluations in parallel."""
results = []
# Get Item function
def process_item(item):
# Calling our Evalution function for Single prompt response pair
eval_result = self.evaluate_single(
prompt=item.get('prompt', ''),
response=item.get('response', ''),
expected_answer=item.get('expected_answer',''),
task_type=item.get('task_type', 'general')
)
# Combining with original metadata
eval_result.update({
'task_id': item.get('task_id', eval_result['scores']['eval_id']),
'agent_name': item.get('agent_name', 'Unknown'),
})
return eval_result
with concurrent.futures.ThreadPoolExecutor() as executor:
future_to_item = {executor.submit(process_item, item): item for item in data}
for future in concurrent.futures.as_completed(future_to_item):
try:
results.append(future.result())
except Exception as exc:
print(f'An item generated an exception: {exc}')
return results
# Instruction Following Evaluation (Prompt, Response)
def _evaluate_instruction_following(self, prompt: str, response: str) -> Tuple[float, str]:
score, checks, passed = 1.0, 0, 0
# Check for negative constraints
negations = re.findall(r"(don't|do not|avoid|without) ([\w\s,]+)", prompt.lower())
for _, constraint_phrase in negations:
checks += 1
words_to_avoid = [w.strip() for w in constraint_phrase.split(',')]
if not any(word in response.lower() for word in words_to_avoid if len(word) > 2):
passed += 1
# Fallback to semantic similarity if no specific instructions found
if checks == 0:
sim = self._semantic_similarity(prompt, response)
return sim, f"No specific constraints found. Score based on semantic similarity ({sim:.2f}) to prompt."
# Final Score calculation
score = passed / checks if checks > 0 else 1.0
reason = f"{passed}/{checks} specific constraints were followed."
return score, reason
# Evaluating Coherence (response)
def _evaluate_coherence(self, response: str) -> Tuple[float, str]:
# Extracting Sentences from Response
doc = self.nlp(response)
sentences = [sent.text for sent in doc.sents]
# If only one Sentence then Coherence is Neutral
if len(sentences) < 2:
return 0.7, "Coherence is neutral for single-sentence responses."
# Fetching Embeddings from our Sentence Model
embeddings = self.sentence_model.encode(sentences)
sims = [cosine_similarity([embeddings[i]], [embeddings[i+1]])[0][0] for i in range(len(sentences)-1)]
score = np.mean(sims)
reason = f"Average sentence-to-sentence similarity score is {score:.2f} across {len(sentences)} sentences."
return score, reason
# Evaluating Accuracy (Response, Expected, Task_type)
def _evaluate_accuracy(self, response: str, expected: str, task_type: str) -> Tuple[float, str]:
sim = self._semantic_similarity(response, expected)
reason = f"Semantic similarity between response and expected answer is {sim:.2f}."
if sim > 0.95:
reason += " (High match)"
elif sim < 0.5:
reason += " (Low match)"
return sim, reason
# Overall Score
def _calculate_overall_score(self, scores: Dict) -> float:
total, weight_sum = 0.0, 0.0
for metric, weight in self.weights.items():
if metric in scores:
total += scores[metric] * weight
weight_sum += weight
return total / weight_sum #if weight_sum > 0 else 0.5
# Explanation Generator, work in progress
def generate_explanation(self, scores: Dict) -> str:
explanation = []
overall = scores.get('overall_score', 0)
explanation.append(f"Overall Score: {overall:.2f}/1.00 - Reflects a weighted average of all dimensions.")
if scores.get('instruction_following', 0) < 0.6:
explanation.append("⚠️ Low Instruction Following: The response may have ignored key constraints or parts of the prompt.")
if scores.get('hallucination_score', 0) < 0.6:
explanation.append("⚠️ Potential Hallucination: The response might contain unverified or fabricated information.")
if scores.get('accuracy', 0) < 0.6 and scores.get('accuracy', 0.5) != 0.5:
explanation.append("⚠️ Low Accuracy: The response significantly differs from the provided expected answer.")
if not explanation[1:]:
explanation.append("✅ Great Performance: The agent performed well across the primary evaluation dimensions.")
return "\n".join(explanation)
# Agent Scores
def get_agent_scores_from_results(self, results: List[Dict]) -> Dict[str, List[float]]:
agent_scores = defaultdict(list)
for result in results:
agent_name = result.get('agent_name', 'Unknown')
overall_score = result.get('scores', {}).get('overall_score', 0)
agent_scores[agent_name].append(overall_score)
return agent_scores
# Helper Functions
def _generate_eval_id(self, prompt: str, response: str) -> str:
return hashlib.md5(f"{prompt}{response}".encode()).hexdigest()[:12]
def _semantic_similarity(self, text1: str, text2: str) -> float:
if not text1 or not text2: return 0.0
emb1 = self.sentence_model.encode([text1])
emb2 = self.sentence_model.encode([text2])
return cosine_similarity(emb1, emb2)[0][0] |