Supastrikas-004 manayporwal07 commited on
Commit
243d8e9
·
verified ·
1 Parent(s): c89cad9

Update evaluator_module.py (#1)

Browse files

- Update evaluator_module.py (114f82fe7eca7efe34d222f4681d4ccba6b1f895)


Co-authored-by: Manay Porwal <manayporwal07@users.noreply.huggingface.co>

Files changed (1) hide show
  1. evaluator_module.py +99 -37
evaluator_module.py CHANGED
@@ -10,7 +10,7 @@ from sklearn.metrics.pairwise import cosine_similarity
10
  import hashlib
11
  from datetime import datetime
12
  import concurrent.futures
13
- import random # For LLM simulation
14
 
15
  class AetherScoreEvaluator:
16
  def __init__(self):
@@ -21,69 +21,118 @@ class AetherScoreEvaluator:
21
  print("Downloading 'en_core_web_sm' spacy model...")
22
  spacy.cli.download("en_core_web_sm")
23
  self.nlp = spacy.load("en_core_web_sm")
 
 
 
 
 
 
 
 
 
24
  self.sentence_model = SentenceTransformer('all-MiniLM-L6-v2')
25
 
26
- # Scoring weights
27
  self.weights = {'instruction_following': 0.25, 'hallucination_score': 0.20,
28
  'assumption_control': 0.20, 'coherence': 0.20, 'accuracy': 0.15}
29
 
30
  # In-memory cache
31
  self.cache = {}
32
 
33
- # --- LLM Judge (Simulated) ---
34
  def _evaluate_with_llm_judge(self, prompt: str, response: str) -> Dict:
35
- """
36
- Simulated LLM Judge for nuanced evaluation.
37
- Replace this with a real API call to a model like Gemini or GPT-4.
38
- """
39
- print("Using simulated LLM Judge...")
40
- # Simulate API call and JSON parsing
41
- simulated_scores = {
42
- "hallucination_score": (random.uniform(0.6, 0.95), "LLM Judge found the facts to be reasonably grounded in the prompt."),
43
- "assumption_control": (random.uniform(0.5, 0.9), "LLM Judge noted minor, acceptable assumptions were made.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
- # In a real scenario, you'd handle API errors and fall back to heuristics.
46
- return simulated_scores
47
 
48
- # --- Main Evaluation Logic ---
49
  def evaluate_single(self, prompt: str, response: str, expected_answer: Optional[str] = None, task_type: str = "general") -> Dict:
 
 
50
  eval_id = self._generate_eval_id(prompt, response)
 
 
51
  if eval_id in self.cache:
52
  return self.cache[eval_id]
53
 
54
  scores, reasons = {}, {}
55
 
56
- # Use LLM Judge for hallucination and assumption control
57
  llm_judge_results = self._evaluate_with_llm_judge(prompt, response)
58
  scores['hallucination_score'], reasons['hallucination_score'] = llm_judge_results['hallucination_score']
59
  scores['assumption_control'], reasons['assumption_control'] = llm_judge_results['assumption_control']
60
 
61
- # Heuristic evaluations for other metrics
62
  scores['instruction_following'], reasons['instruction_following'] = self._evaluate_instruction_following(prompt, response)
63
  scores['coherence'], reasons['coherence'] = self._evaluate_coherence(response)
64
  scores['accuracy'], reasons['accuracy'] = self._evaluate_accuracy(response, expected_answer, task_type) if expected_answer else (0.5, "No expected answer provided.")
65
-
 
66
  scores['overall_score'] = self._calculate_overall_score(scores)
67
- reasons['overall_score'] = f"Weighted average based on component scores."
68
-
 
69
  scores.update({'eval_id': eval_id, 'timestamp': datetime.now().isoformat(), 'task_type': task_type})
70
-
 
71
  result = {"scores": scores, "reasons": reasons}
 
 
72
  self.cache[eval_id] = result
 
73
  return result
74
-
75
  def evaluate_batch(self, data: List[Dict], mode: str = "comprehensive") -> List[Dict]:
76
  """Process a batch of evaluations in parallel."""
77
- results = []
78
 
 
 
 
79
  def process_item(item):
 
80
  eval_result = self.evaluate_single(
81
  prompt=item.get('prompt', ''),
82
  response=item.get('response', ''),
83
- expected_answer=item.get('expected_answer'),
84
  task_type=item.get('task_type', 'general')
85
  )
86
- # Combine with original metadata
87
  eval_result.update({
88
  'task_id': item.get('task_id', eval_result['scores']['eval_id']),
89
  'agent_name': item.get('agent_name', 'Unknown'),
@@ -100,7 +149,7 @@ class AetherScoreEvaluator:
100
 
101
  return results
102
 
103
- # --- Scoring Sub-components ---
104
  def _evaluate_instruction_following(self, prompt: str, response: str) -> Tuple[float, str]:
105
  score, checks, passed = 1.0, 0, 0
106
 
@@ -116,24 +165,35 @@ class AetherScoreEvaluator:
116
  if checks == 0:
117
  sim = self._semantic_similarity(prompt, response)
118
  return sim, f"No specific constraints found. Score based on semantic similarity ({sim:.2f}) to prompt."
119
-
 
120
  score = passed / checks if checks > 0 else 1.0
121
  reason = f"{passed}/{checks} specific constraints were followed."
 
122
  return score, reason
123
-
 
124
  def _evaluate_coherence(self, response: str) -> Tuple[float, str]:
 
 
125
  doc = self.nlp(response)
126
  sentences = [sent.text for sent in doc.sents]
 
 
127
  if len(sentences) < 2:
128
  return 0.7, "Coherence is neutral for single-sentence responses."
129
-
 
130
  embeddings = self.sentence_model.encode(sentences)
131
  sims = [cosine_similarity([embeddings[i]], [embeddings[i+1]])[0][0] for i in range(len(sentences)-1)]
132
- avg_sim = np.mean(sims) if sims else 0
133
- score = max(0.0, min(1.0, avg_sim))
 
134
  reason = f"Average sentence-to-sentence similarity score is {score:.2f} across {len(sentences)} sentences."
135
  return score, reason
136
 
 
 
137
  def _evaluate_accuracy(self, response: str, expected: str, task_type: str) -> Tuple[float, str]:
138
  sim = self._semantic_similarity(response, expected)
139
  reason = f"Semantic similarity between response and expected answer is {sim:.2f}."
@@ -142,17 +202,18 @@ class AetherScoreEvaluator:
142
  elif sim < 0.5:
143
  reason += " (Low match)"
144
  return sim, reason
145
-
 
146
  def _calculate_overall_score(self, scores: Dict) -> float:
147
  total, weight_sum = 0.0, 0.0
148
  for metric, weight in self.weights.items():
149
  if metric in scores:
150
  total += scores[metric] * weight
151
  weight_sum += weight
152
- return total / weight_sum if weight_sum > 0 else 0.5
153
 
 
154
  def generate_explanation(self, scores: Dict) -> str:
155
- """Generate a high-level summary explanation."""
156
  explanation = []
157
  overall = scores.get('overall_score', 0)
158
  explanation.append(f"Overall Score: {overall:.2f}/1.00 - Reflects a weighted average of all dimensions.")
@@ -165,10 +226,11 @@ class AetherScoreEvaluator:
165
  explanation.append("⚠️ Low Accuracy: The response significantly differs from the provided expected answer.")
166
 
167
  if not explanation[1:]:
168
- explanation.append("✅ Solid Performance: The agent performed well across the primary evaluation dimensions.")
169
 
170
  return "\n".join(explanation)
171
 
 
172
  def get_agent_scores_from_results(self, results: List[Dict]) -> Dict[str, List[float]]:
173
  agent_scores = defaultdict(list)
174
  for result in results:
@@ -177,10 +239,10 @@ class AetherScoreEvaluator:
177
  agent_scores[agent_name].append(overall_score)
178
  return agent_scores
179
 
180
- # --- Helper Methods ---
181
  def _generate_eval_id(self, prompt: str, response: str) -> str:
182
  return hashlib.md5(f"{prompt}{response}".encode()).hexdigest()[:12]
183
-
184
  def _semantic_similarity(self, text1: str, text2: str) -> float:
185
  if not text1 or not text2: return 0.0
186
  emb1 = self.sentence_model.encode([text1])
 
10
  import hashlib
11
  from datetime import datetime
12
  import concurrent.futures
13
+ import random
14
 
15
  class AetherScoreEvaluator:
16
  def __init__(self):
 
21
  print("Downloading 'en_core_web_sm' spacy model...")
22
  spacy.cli.download("en_core_web_sm")
23
  self.nlp = spacy.load("en_core_web_sm")
24
+
25
+ # LLM Judge Model
26
+ self.judge_model = pipeline(
27
+ "text2text-generation",
28
+ model="google/flan-t5-small",
29
+ device=-1 # 0 for GPU
30
+ )
31
+
32
+ # Sentence Transformer for Sentence ----> Embedding
33
  self.sentence_model = SentenceTransformer('all-MiniLM-L6-v2')
34
 
35
+ # Scoring weights # Domain Specific weights can be added for better results
36
  self.weights = {'instruction_following': 0.25, 'hallucination_score': 0.20,
37
  'assumption_control': 0.20, 'coherence': 0.20, 'accuracy': 0.15}
38
 
39
  # In-memory cache
40
  self.cache = {}
41
 
42
+ # LLM Judge
43
  def _evaluate_with_llm_judge(self, prompt: str, response: str) -> Dict:
44
+
45
+ print("Using HF model LLM Judge...")
46
+ # Building Query for LLM Judge
47
+ query = (
48
+ f"Prompt: {prompt}\n"
49
+ f"Response: {response}\n\n"
50
+ "Evaluate the response and provide:\n"
51
+ "hallucination_score: float between 0 and 1 (1 = fully grounded, 0 = full hallucination)\n"
52
+ "assumption_control: float between 0 and 1 (1 = no assumptions, 0 = many unsupported assumptions)\n"
53
+ "Then explain briefly in one or two sentences."
54
+ )
55
+
56
+ # Calling our LLM
57
+ try:
58
+ result = self.judge_model(query, max_new_tokens=128, truncation=True)
59
+ output = result[0]['generated_text']
60
+ except Exception as e:
61
+ return {
62
+ "hallucination_score": (0.0, f"HF model failed: {e}"),
63
+ "assumption_control": (0.0, f"HF model failed: {e}")
64
+ }
65
+
66
+ # Parsing scores from model output
67
+ halluc_score = 0.0
68
+ assumption_score = 0.0
69
+ explanation = output
70
+
71
+ match_h = re.search(r"hallucination[_ ]?score[: ]?([01](?:\.\d+)?)", output, re.I)
72
+ match_a = re.search(r"assumption[_ ]?control[: ]?([01](?:\.\d+)?)", output, re.I)
73
+
74
+ if match_h:
75
+ halluc_score = float(match_h.group(1))
76
+ if match_a:
77
+ assumption_score = float(match_a.group(1))
78
+
79
+ return {
80
+ "hallucination_score": (halluc_score, explanation),
81
+ "assumption_control": (assumption_score, explanation)
82
  }
 
 
83
 
84
+ # Single Evaluation # Inputs-->> Prompt, Agent Response, Expected Answer(Optional), Agent Name and Task type( General, QA, Summarizaton)etc
85
  def evaluate_single(self, prompt: str, response: str, expected_answer: Optional[str] = None, task_type: str = "general") -> Dict:
86
+
87
+ # Generating Eval ID
88
  eval_id = self._generate_eval_id(prompt, response)
89
+
90
+ # If already stored in cache direclty we can return from there.
91
  if eval_id in self.cache:
92
  return self.cache[eval_id]
93
 
94
  scores, reasons = {}, {}
95
 
96
+ # Taking Back scores and reasons of hallucination and assumption control from LLM Judge
97
  llm_judge_results = self._evaluate_with_llm_judge(prompt, response)
98
  scores['hallucination_score'], reasons['hallucination_score'] = llm_judge_results['hallucination_score']
99
  scores['assumption_control'], reasons['assumption_control'] = llm_judge_results['assumption_control']
100
 
101
+ # Evaluating Instruction Following, Coherence and Accuracy
102
  scores['instruction_following'], reasons['instruction_following'] = self._evaluate_instruction_following(prompt, response)
103
  scores['coherence'], reasons['coherence'] = self._evaluate_coherence(response)
104
  scores['accuracy'], reasons['accuracy'] = self._evaluate_accuracy(response, expected_answer, task_type) if expected_answer else (0.5, "No expected answer provided.")
105
+
106
+ # Calculating Overall Score
107
  scores['overall_score'] = self._calculate_overall_score(scores)
108
+ reasons['overall_score'] = f" Weighted Average Score based on component scores."
109
+
110
+ # Updating Eval ID, Timestamp and Task Type in Scores
111
  scores.update({'eval_id': eval_id, 'timestamp': datetime.now().isoformat(), 'task_type': task_type})
112
+
113
+ # Updating scores(Eval ID, timestamp and task_type) and reasons(all scores) in result
114
  result = {"scores": scores, "reasons": reasons}
115
+
116
+ #Storing results with corresponding Eval ID in cache
117
  self.cache[eval_id] = result
118
+
119
  return result
120
+ # Batch Evaluation # Input of JSON/CSV file
121
  def evaluate_batch(self, data: List[Dict], mode: str = "comprehensive") -> List[Dict]:
122
  """Process a batch of evaluations in parallel."""
 
123
 
124
+ results = []
125
+
126
+ # Get Item function
127
  def process_item(item):
128
+ # Calling our Evalution function for Single prompt response pair
129
  eval_result = self.evaluate_single(
130
  prompt=item.get('prompt', ''),
131
  response=item.get('response', ''),
132
+ expected_answer=item.get('expected_answer',''),
133
  task_type=item.get('task_type', 'general')
134
  )
135
+ # Combining with original metadata
136
  eval_result.update({
137
  'task_id': item.get('task_id', eval_result['scores']['eval_id']),
138
  'agent_name': item.get('agent_name', 'Unknown'),
 
149
 
150
  return results
151
 
152
+ # Instruction Following Evaluation (Prompt, Response)
153
  def _evaluate_instruction_following(self, prompt: str, response: str) -> Tuple[float, str]:
154
  score, checks, passed = 1.0, 0, 0
155
 
 
165
  if checks == 0:
166
  sim = self._semantic_similarity(prompt, response)
167
  return sim, f"No specific constraints found. Score based on semantic similarity ({sim:.2f}) to prompt."
168
+
169
+ # Final Score calculation
170
  score = passed / checks if checks > 0 else 1.0
171
  reason = f"{passed}/{checks} specific constraints were followed."
172
+
173
  return score, reason
174
+
175
+ # Evaluating Coherence (response)
176
  def _evaluate_coherence(self, response: str) -> Tuple[float, str]:
177
+
178
+ # Extracting Sentences from Response
179
  doc = self.nlp(response)
180
  sentences = [sent.text for sent in doc.sents]
181
+
182
+ # If only one Sentence then Coherence is Neutral
183
  if len(sentences) < 2:
184
  return 0.7, "Coherence is neutral for single-sentence responses."
185
+
186
+ # Fetching Embeddings from our Sentence Model
187
  embeddings = self.sentence_model.encode(sentences)
188
  sims = [cosine_similarity([embeddings[i]], [embeddings[i+1]])[0][0] for i in range(len(sentences)-1)]
189
+
190
+ score = np.mean(sims)
191
+
192
  reason = f"Average sentence-to-sentence similarity score is {score:.2f} across {len(sentences)} sentences."
193
  return score, reason
194
 
195
+
196
+ # Evaluating Accuracy (Response, Expected, Task_type)
197
  def _evaluate_accuracy(self, response: str, expected: str, task_type: str) -> Tuple[float, str]:
198
  sim = self._semantic_similarity(response, expected)
199
  reason = f"Semantic similarity between response and expected answer is {sim:.2f}."
 
202
  elif sim < 0.5:
203
  reason += " (Low match)"
204
  return sim, reason
205
+
206
+ # Overall Score
207
  def _calculate_overall_score(self, scores: Dict) -> float:
208
  total, weight_sum = 0.0, 0.0
209
  for metric, weight in self.weights.items():
210
  if metric in scores:
211
  total += scores[metric] * weight
212
  weight_sum += weight
213
+ return total / weight_sum #if weight_sum > 0 else 0.5
214
 
215
+ # Explanation Generator, work in progress
216
  def generate_explanation(self, scores: Dict) -> str:
 
217
  explanation = []
218
  overall = scores.get('overall_score', 0)
219
  explanation.append(f"Overall Score: {overall:.2f}/1.00 - Reflects a weighted average of all dimensions.")
 
226
  explanation.append("⚠️ Low Accuracy: The response significantly differs from the provided expected answer.")
227
 
228
  if not explanation[1:]:
229
+ explanation.append("✅ Great Performance: The agent performed well across the primary evaluation dimensions.")
230
 
231
  return "\n".join(explanation)
232
 
233
+ # Agent Scores
234
  def get_agent_scores_from_results(self, results: List[Dict]) -> Dict[str, List[float]]:
235
  agent_scores = defaultdict(list)
236
  for result in results:
 
239
  agent_scores[agent_name].append(overall_score)
240
  return agent_scores
241
 
242
+ # Helper Functions
243
  def _generate_eval_id(self, prompt: str, response: str) -> str:
244
  return hashlib.md5(f"{prompt}{response}".encode()).hexdigest()[:12]
245
+
246
  def _semantic_similarity(self, text1: str, text2: str) -> float:
247
  if not text1 or not text2: return 0.0
248
  emb1 = self.sentence_model.encode([text1])