| """ |
| eval/run_ragas.py |
| DeepEval FaithfulnessMetric with Mistral 7B as local judge. |
| |
| RAGAS 0.1.21 is incompatible with llama-cpp-python's synchronous Llama object |
| (requires agenerate_prompt async method). We use DeepEval's FaithfulnessMetric |
| instead, which supports any custom LLM via DeepEvalBaseLLM and runs synchronously |
| when async_mode=False. |
| |
| DeepEval FaithfulnessMetric definition (identical to RAGAS): |
| 1. LLM extracts all factual claims from the generated response |
| 2. LLM checks each claim against retrieved context — truthful if not contradicted |
| 3. Score = number of truthful claims / total claims |
| |
| Mistral 7B Instruct Q4_K_M was confirmed to produce valid JSON for claim extraction |
| (tested before implementation). No JSON confinement library required. |
| |
| Citation: DeepEval FaithfulnessMetric — https://deepeval.com/docs/metrics-faithfulness |
| """ |
|
|
| import sys, json |
| sys.path.insert(0, "src") |
| sys.path.insert(0, ".") |
|
|
| from deepeval.models.base_model import DeepEvalBaseLLM |
| from deepeval.metrics import FaithfulnessMetric |
| from deepeval.test_case import LLMTestCase |
| from deepeval import evaluate as deepeval_evaluate |
| from pipeline.pipeline import EmpathRAGPipeline |
|
|
| PROMPTS_PATH = "eval/test_prompts.json" |
| RESULTS_PATH = "eval/ragas_results.json" |
| N_EVAL = 40 |
|
|
|
|
| class MistralJudge(DeepEvalBaseLLM): |
| """ |
| Wraps llama-cpp-python's Llama instance as a DeepEvalBaseLLM judge. |
| Mistral 7B Instruct Q4_K_M confirmed to produce valid JSON for |
| claim extraction prompts (tested independently). |
| """ |
| def __init__(self, llm): |
| self._llm = llm |
|
|
| def load_model(self): |
| return self._llm |
|
|
| def generate(self, prompt: str) -> str: |
| from llama_cpp import LlamaGrammar |
| import json as _json |
| |
| |
| |
| |
| |
| prompt_lower = prompt.lower() |
| if '"truths"' in prompt_lower or 'truths key' in prompt_lower: |
| |
| schema = _json.dumps({ |
| "type": "object", |
| "properties": {"truths": {"type": "array", "items": {"type": "string"}}} |
| }) |
| elif '"claims"' in prompt_lower or 'claims' in prompt_lower: |
| |
| schema = _json.dumps({ |
| "type": "object", |
| "properties": {"claims": {"type": "array", "items": {"type": "string"}}} |
| }) |
| else: |
| |
| schema = _json.dumps({ |
| "type": "object", |
| "properties": { |
| "verdicts": { |
| "type": "array", |
| "items": { |
| "type": "object", |
| "properties": { |
| "verdict": {"type": "string", "enum": ["yes", "no", "idk"]}, |
| "reason": {"type": "string"} |
| } |
| } |
| } |
| } |
| }) |
| try: |
| grammar = LlamaGrammar.from_json_schema(schema, verbose=False) |
| out = self._llm(prompt, max_tokens=1024, temperature=0.0, grammar=grammar, stop=["[INST]"]) |
| except Exception as e: |
| |
| print(f"[MistralJudge] Grammar fallback: {e}") |
| out = self._llm(prompt, max_tokens=1024, temperature=0.0, stop=["[INST]"]) |
| return out["choices"][0]["text"].strip() |
|
|
| async def a_generate(self, prompt: str) -> str: |
| |
| |
| |
| return self.generate(prompt) |
|
|
| def get_model_name(self) -> str: |
| return "Mistral-7B-Instruct-v0.2-Q4_K_M (local)" |
|
|
|
|
| def run_faithfulness_eval(): |
| with open(PROMPTS_PATH) as f: |
| prompts = json.load(f) |
|
|
| print("Initialising pipeline (use_real_guardrail=False for speed)...") |
| pipeline = EmpathRAGPipeline( |
| use_real_guardrail=False, |
| allow_stub_guardrail=True, |
| guardrail_threshold=0.5, |
| ) |
|
|
| |
| |
| original_check = pipeline.guardrail.check |
| def fast_check(text, threshold=0.5, skip_ig=False): |
| return original_check(text, threshold=threshold, skip_ig=True) |
| pipeline.guardrail.check = fast_check |
|
|
| print("Wrapping Mistral as DeepEval judge (async_mode=False)...") |
| judge = MistralJudge(pipeline.llm) |
| metric = FaithfulnessMetric( |
| model=judge, |
| threshold=0.5, |
| async_mode=False, |
| include_reason=False, |
| verbose_mode=False, |
| ) |
|
|
| test_cases = [] |
| count = 0 |
|
|
| print(f"Collecting pipeline outputs (target: {N_EVAL} non-crisis prompts)...") |
| for prompt in prompts: |
| if count >= N_EVAL: |
| break |
| result = pipeline.run(prompt["text"]) |
| if result["crisis"] or not result["retrieved_chunks"]: |
| continue |
|
|
| test_cases.append(LLMTestCase( |
| input=prompt["text"], |
| actual_output=result["response"], |
| retrieval_context=result["retrieved_chunks"], |
| )) |
| count += 1 |
| print(f" [{count:02d}/{N_EVAL}] {prompt['emotion']:<12} | {prompt['text'][:50]}...") |
|
|
| print(f"\nRunning DeepEval FaithfulnessMetric on {len(test_cases)} test cases...") |
| print("(Each case: Mistral extracts claims, then verifies each against context)") |
|
|
| scores = [] |
| for i, tc in enumerate(test_cases): |
| metric.measure(tc) |
| score = metric.score |
| scores.append(score) |
| print(f" [{i+1:02d}/{len(test_cases)}] faithfulness={score:.3f}") |
|
|
| mean_score = sum(scores) / len(scores) if scores else 0.0 |
| passed = mean_score >= 0.5 |
|
|
| print(f"\nFaithfulness Results (DeepEval FaithfulnessMetric):") |
| print(f" Mean faithfulness: {mean_score:.4f}") |
| print(f" Threshold: 0.5 (DeepEval default)") |
| print(f" Target for paper: > 0.65") |
| print(f" {'PASS' if mean_score >= 0.65 else 'BELOW 0.65 TARGET' if mean_score >= 0.5 else 'BELOW THRESHOLD'}") |
| print(f" n_evaluated: {len(scores)}") |
|
|
| output = { |
| "method": "DeepEval FaithfulnessMetric (Mistral-7B judge, async_mode=False)", |
| "faithfulness": round(mean_score, 4), |
| "target": 0.65, |
| "pass": mean_score >= 0.65, |
| "n_evaluated": len(scores), |
| "per_sample": [round(s, 4) for s in scores], |
| "score_distribution": { |
| "min": round(min(scores), 4) if scores else None, |
| "max": round(max(scores), 4) if scores else None, |
| "median": round(sorted(scores)[len(scores)//2], 4) if scores else None, |
| } |
| } |
| with open(RESULTS_PATH, "w") as f: |
| json.dump(output, f, indent=2) |
| print(f"Results saved to {RESULTS_PATH}") |
|
|
|
|
| if __name__ == "__main__": |
| run_faithfulness_eval() |
|
|