Mukul Rayana commited on
Commit
d02b074
·
1 Parent(s): 9bce0e0

feat: DeepEval FaithfulnessMetric with Mistral judge, async_mode=False (replaces RAGAS, Day 15)

Browse files
Files changed (1) hide show
  1. eval/run_ragas.py +108 -47
eval/run_ragas.py CHANGED
@@ -1,88 +1,149 @@
1
  """
2
  eval/run_ragas.py
3
- RAGAS faithfulness evaluation using local Mistral 7B as judge (no OpenAI).
4
- Evaluates whether generated responses are faithful to retrieved context.
5
- Saves results to eval/ragas_results.json
6
-
7
- RAGAS 0.1.21 API:
8
- - ragas.evaluate(Dataset, metrics=[faithfulness])
9
- - Dataset needs columns: question, answer, contexts (list of strings)
10
- - LLM judge configured via ragas.llms with LangchainLLMWrapper
 
 
 
 
 
 
 
 
11
  """
12
 
13
  import sys, json
14
  sys.path.insert(0, "src")
 
15
 
16
- from datasets import Dataset
17
- from ragas import evaluate as ragas_evaluate
18
- from ragas.metrics import faithfulness
19
- from ragas.llms import LangchainLLMWrapper
20
  from pipeline.pipeline import EmpathRAGPipeline
21
 
22
  PROMPTS_PATH = "eval/test_prompts.json"
23
  RESULTS_PATH = "eval/ragas_results.json"
24
- MISTRAL_PATH = "models/generator/mistral-7b-instruct-v0.2.Q4_K_M.gguf"
25
- N_EVAL = 40 # evaluate first 40 non-crisis prompts
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- def run_ragas_eval():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  with open(PROMPTS_PATH) as f:
29
  prompts = json.load(f)
30
 
31
- print("Initialising pipeline...")
32
- pipeline = EmpathRAGPipeline(use_real_guardrail=True, guardrail_threshold=0.5)
33
 
 
 
34
  original_check = pipeline.guardrail.check
35
  def fast_check(text, threshold=0.5, skip_ig=False):
36
  return original_check(text, threshold=threshold, skip_ig=True)
37
  pipeline.guardrail.check = fast_check
38
 
39
- print("Configuring RAGAS to reuse pipeline Mistral instance (avoids double VRAM load)...")
40
- # Reuse the pipeline's already-loaded Mistral instance as RAGAS judge
41
- # This prevents a second LlamaCpp from loading and causing OOM on RTX 3060 6GB
42
- wrapped_llm = LangchainLLMWrapper(pipeline.llm)
43
- faithfulness.llm = wrapped_llm
 
 
 
 
44
 
45
- questions = []
46
- answers = []
47
- contexts = []
48
 
49
- count = 0
50
  print(f"Collecting pipeline outputs (target: {N_EVAL} non-crisis prompts)...")
51
  for prompt in prompts:
52
  if count >= N_EVAL:
53
  break
54
  result = pipeline.run(prompt["text"])
55
- if result["crisis"]:
56
- continue # skip crisis intercepts — no retrieved context to evaluate
57
- questions.append(prompt["text"])
58
- answers.append(result["response"])
59
- contexts.append(result["retrieved_chunks"])
 
 
 
60
  count += 1
61
  print(f" [{count:02d}/{N_EVAL}] {prompt['emotion']:<12} | {prompt['text'][:50]}...")
62
 
63
- print(f"\nBuilding RAGAS dataset ({len(questions)} samples)...")
64
- ds = Dataset.from_dict({
65
- "question": questions,
66
- "answer": answers,
67
- "contexts": contexts,
68
- })
 
 
 
69
 
70
- print("Running RAGAS faithfulness evaluation (local Mistral judge)...")
71
- result = ragas_evaluate(ds, metrics=[faithfulness])
72
- score = result["faithfulness"]
73
 
74
- print(f"\nRAGAS Faithfulness: {score:.4f} (target: > 0.65)")
75
- print("PASS" if score >= 0.65 else "BELOW TARGET (target 0.65)")
 
 
 
 
76
 
77
  output = {
78
- "faithfulness": round(float(score), 4),
79
- "target": 0.65,
80
- "pass": float(score) >= 0.65,
81
- "n_evaluated": len(questions),
 
 
 
 
 
 
 
82
  }
83
  with open(RESULTS_PATH, "w") as f:
84
  json.dump(output, f, indent=2)
85
  print(f"Results saved to {RESULTS_PATH}")
86
 
 
87
  if __name__ == "__main__":
88
- run_ragas_eval()
 
1
  """
2
  eval/run_ragas.py
3
+ DeepEval FaithfulnessMetric with Mistral 7B as local judge.
4
+
5
+ RAGAS 0.1.21 is incompatible with llama-cpp-python's synchronous Llama object
6
+ (requires agenerate_prompt async method). We use DeepEval's FaithfulnessMetric
7
+ instead, which supports any custom LLM via DeepEvalBaseLLM and runs synchronously
8
+ when async_mode=False.
9
+
10
+ DeepEval FaithfulnessMetric definition (identical to RAGAS):
11
+ 1. LLM extracts all factual claims from the generated response
12
+ 2. LLM checks each claim against retrieved context — truthful if not contradicted
13
+ 3. Score = number of truthful claims / total claims
14
+
15
+ Mistral 7B Instruct Q4_K_M was confirmed to produce valid JSON for claim extraction
16
+ (tested before implementation). No JSON confinement library required.
17
+
18
+ Citation: DeepEval FaithfulnessMetric — https://deepeval.com/docs/metrics-faithfulness
19
  """
20
 
21
  import sys, json
22
  sys.path.insert(0, "src")
23
+ sys.path.insert(0, ".")
24
 
25
+ from deepeval.models.base_model import DeepEvalBaseLLM
26
+ from deepeval.metrics import FaithfulnessMetric
27
+ from deepeval.test_case import LLMTestCase
28
+ from deepeval import evaluate as deepeval_evaluate
29
  from pipeline.pipeline import EmpathRAGPipeline
30
 
31
  PROMPTS_PATH = "eval/test_prompts.json"
32
  RESULTS_PATH = "eval/ragas_results.json"
33
+ N_EVAL = 40
34
+
35
+
36
+ class MistralJudge(DeepEvalBaseLLM):
37
+ """
38
+ Wraps llama-cpp-python's Llama instance as a DeepEvalBaseLLM judge.
39
+ Mistral 7B Instruct Q4_K_M confirmed to produce valid JSON for
40
+ claim extraction prompts (tested independently).
41
+ """
42
+ def __init__(self, llm):
43
+ self._llm = llm
44
+
45
+ def load_model(self):
46
+ return self._llm
47
 
48
+ def generate(self, prompt: str) -> str:
49
+ out = self._llm(
50
+ prompt,
51
+ max_tokens=1024,
52
+ temperature=0.0,
53
+ stop=["[INST]"],
54
+ )
55
+ return out["choices"][0]["text"].strip()
56
+
57
+ async def a_generate(self, prompt: str) -> str:
58
+ # DeepEval calls a_generate when async_mode=True.
59
+ # We set async_mode=False so this should never be called,
60
+ # but implement it as a synchronous fallback for safety.
61
+ return self.generate(prompt)
62
+
63
+ def get_model_name(self) -> str:
64
+ return "Mistral-7B-Instruct-v0.2-Q4_K_M (local)"
65
+
66
+
67
+ def run_faithfulness_eval():
68
  with open(PROMPTS_PATH) as f:
69
  prompts = json.load(f)
70
 
71
+ print("Initialising pipeline (use_real_guardrail=False for speed)...")
72
+ pipeline = EmpathRAGPipeline(use_real_guardrail=False, guardrail_threshold=0.5)
73
 
74
+ # Monkey-patch guardrail to skip IG (no-op since stub is active, but kept for
75
+ # consistency with other eval scripts in case real guardrail is swapped in)
76
  original_check = pipeline.guardrail.check
77
  def fast_check(text, threshold=0.5, skip_ig=False):
78
  return original_check(text, threshold=threshold, skip_ig=True)
79
  pipeline.guardrail.check = fast_check
80
 
81
+ print("Wrapping Mistral as DeepEval judge (async_mode=False)...")
82
+ judge = MistralJudge(pipeline.llm)
83
+ metric = FaithfulnessMetric(
84
+ model=judge,
85
+ threshold=0.5,
86
+ async_mode=False, # synchronous — avoids agenerate_prompt error
87
+ include_reason=False, # speeds up evaluation — we only need the score
88
+ verbose_mode=False,
89
+ )
90
 
91
+ test_cases = []
92
+ count = 0
 
93
 
 
94
  print(f"Collecting pipeline outputs (target: {N_EVAL} non-crisis prompts)...")
95
  for prompt in prompts:
96
  if count >= N_EVAL:
97
  break
98
  result = pipeline.run(prompt["text"])
99
+ if result["crisis"] or not result["retrieved_chunks"]:
100
+ continue
101
+
102
+ test_cases.append(LLMTestCase(
103
+ input=prompt["text"],
104
+ actual_output=result["response"],
105
+ retrieval_context=result["retrieved_chunks"],
106
+ ))
107
  count += 1
108
  print(f" [{count:02d}/{N_EVAL}] {prompt['emotion']:<12} | {prompt['text'][:50]}...")
109
 
110
+ print(f"\nRunning DeepEval FaithfulnessMetric on {len(test_cases)} test cases...")
111
+ print("(Each case: Mistral extracts claims, then verifies each against context)")
112
+
113
+ scores = []
114
+ for i, tc in enumerate(test_cases):
115
+ metric.measure(tc)
116
+ score = metric.score
117
+ scores.append(score)
118
+ print(f" [{i+1:02d}/{len(test_cases)}] faithfulness={score:.3f}")
119
 
120
+ mean_score = sum(scores) / len(scores) if scores else 0.0
121
+ passed = mean_score >= 0.5 # DeepEval default threshold is 0.5
 
122
 
123
+ print(f"\nFaithfulness Results (DeepEval FaithfulnessMetric):")
124
+ print(f" Mean faithfulness: {mean_score:.4f}")
125
+ print(f" Threshold: 0.5 (DeepEval default)")
126
+ print(f" Target for paper: > 0.65")
127
+ print(f" {'PASS' if mean_score >= 0.65 else 'BELOW 0.65 TARGET' if mean_score >= 0.5 else 'BELOW THRESHOLD'}")
128
+ print(f" n_evaluated: {len(scores)}")
129
 
130
  output = {
131
+ "method": "DeepEval FaithfulnessMetric (Mistral-7B judge, async_mode=False)",
132
+ "faithfulness": round(mean_score, 4),
133
+ "target": 0.65,
134
+ "pass": mean_score >= 0.65,
135
+ "n_evaluated": len(scores),
136
+ "per_sample": [round(s, 4) for s in scores],
137
+ "score_distribution": {
138
+ "min": round(min(scores), 4) if scores else None,
139
+ "max": round(max(scores), 4) if scores else None,
140
+ "median": round(sorted(scores)[len(scores)//2], 4) if scores else None,
141
+ }
142
  }
143
  with open(RESULTS_PATH, "w") as f:
144
  json.dump(output, f, indent=2)
145
  print(f"Results saved to {RESULTS_PATH}")
146
 
147
+
148
  if __name__ == "__main__":
149
+ run_faithfulness_eval()