File size: 7,642 Bytes
78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 c9853ac 5405e38 ce15608 c9853ac ce15608 5405e38 c9853ac 5405e38 c9853ac 5405e38 c9853ac d02b074 78fc1e6 d02b074 81deeef 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 78fc1e6 d02b074 | 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 | """
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
# DeepEval FaithfulnessMetric makes 3 types of calls:
# 1. Extract truths from retrieval_context → {"truths": ["...", "..."]}
# 2. Extract claims from actual_output → {"claims": ["...", "..."]}
# 3. Verify claims against truths → {"verdicts": [{"verdict": "yes/no", "reason": "..."}]}
# Detect which call type from prompt keywords and use appropriate schema.
prompt_lower = prompt.lower()
if '"truths"' in prompt_lower or 'truths key' in prompt_lower:
# Call 1: extract truths from context
schema = _json.dumps({
"type": "object",
"properties": {"truths": {"type": "array", "items": {"type": "string"}}}
})
elif '"claims"' in prompt_lower or 'claims' in prompt_lower:
# Call 2: extract claims from output
schema = _json.dumps({
"type": "object",
"properties": {"claims": {"type": "array", "items": {"type": "string"}}}
})
else:
# Call 3: verify claims (verdicts)
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:
# Fallback: no grammar if schema compilation fails
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:
# DeepEval calls a_generate when async_mode=True.
# We set async_mode=False so this should never be called,
# but implement it as a synchronous fallback for safety.
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,
)
# Monkey-patch guardrail to skip IG (no-op since stub is active, but kept for
# consistency with other eval scripts in case real guardrail is swapped in)
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, # synchronous — avoids agenerate_prompt error
include_reason=False, # speeds up evaluation — we only need the score
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 # DeepEval default threshold is 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()
|