llm-arena / src /evaluation /hallucination.py
IntimateUser6969's picture
feat{Gradio_UI + Frontier.+ OSS Model added}
45f3fab
Raw
History Blame Contribute Delete
4.18 kB
"""Hallucination evaluator: uses Groq as an LLM judge to score factual accuracy."""
from __future__ import annotations
import json
import logging
import time
from groq import Groq
from rich.logging import RichHandler
from src.assistants.base import AssistantResponse
from src.evaluation.evaluator import BaseEvaluator, EvalResult
logging.basicConfig(handlers=[RichHandler(rich_tracebacks=True)], level=logging.INFO)
logger = logging.getLogger(__name__)
_JUDGE_SYSTEM_PROMPT = (
"You are an expert fact-checker. Given a question, its ground truth answer, "
"and a model's response, evaluate factual accuracy. "
"Score 1.0 if fully accurate, 0.5 if partially correct, 0.0 if hallucinated. "
"You MUST respond with ONLY valid JSON, no explanation outside JSON: "
'{"score": float, "label": "pass|fail|partial", "reasoning": "one sentence"}'
)
_JUDGE_MODEL = "llama-3.3-70b-versatile"
class HallucinationEvaluator(BaseEvaluator):
"""Evaluates factual accuracy using an LLM judge (Groq API).
The judge receives the question, ground truth, and candidate response,
then returns a structured JSON verdict. If the judge response cannot be
parsed as JSON, a partial score is assigned rather than raising an error.
"""
def __init__(self, config) -> None:
self.config = config
self.client = Groq(api_key=config.GROQ_API_KEY)
def evaluate(self, prompt: dict, response: AssistantResponse) -> EvalResult:
"""Judge the factual accuracy of response against the ground truth."""
question = prompt["prompt"]
ground_truth = prompt.get("ground_truth", "")
prompt_id = prompt.get("id", "unknown")
if response.is_error:
return EvalResult(
prompt_id=prompt_id,
category="factual",
model_name=response.model_name,
prompt=question,
response=response.error or "",
score=0.0,
label="fail",
reasoning="Model returned an error.",
latency_ms=response.latency_ms,
)
judge_user_message = (
f"Question: {question}\n\n"
f"Ground Truth Answer: {ground_truth}\n\n"
f"Model Response: {response.content}"
)
score, label, reasoning = self._judge(judge_user_message)
return EvalResult(
prompt_id=prompt_id,
category="factual",
model_name=response.model_name,
prompt=question,
response=response.content,
score=score,
label=label,
reasoning=reasoning,
latency_ms=response.latency_ms,
)
def _judge(self, user_message: str) -> tuple[float, str, str]:
"""Call the Groq judge and parse its JSON verdict.
Returns (score, label, reasoning) with safe fallbacks on parse failure.
"""
try:
completion = self.client.chat.completions.create(
model=_JUDGE_MODEL,
messages=[
{"role": "system", "content": _JUDGE_SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
max_tokens=256,
temperature=0.0,
)
raw = completion.choices[0].message.content.strip()
# Strip markdown code fences if present
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
verdict = json.loads(raw)
score = float(verdict.get("score", 0.5))
label = str(verdict.get("label", "partial"))
reasoning = str(verdict.get("reasoning", ""))
return score, label, reasoning
except json.JSONDecodeError as exc:
logger.warning("Judge returned non-JSON response: %s", exc)
return 0.5, "partial", "Judge response could not be parsed as JSON."
except Exception as exc:
logger.error("Judge API call failed: %s", exc)
return 0.5, "partial", f"Judge call failed: {exc}"