from aimu.evals import HAS_DEEPEVAL, DeepEvalModel
from aimu.models import OllamaClient, OllamaModel
# Use any BaseModelClient as the judge.
# A stronger model produces more consistent judgments.
# from aimu.models import AnthropicClient, AnthropicModel
# judge_client = AnthropicClient(AnthropicModel.CLAUDE_SONNET_4)
print(f"DeepEval available: {HAS_DEEPEVAL}")
judge_client = OllamaClient(OllamaModel.QWEN_3_5_9B)
judge = DeepEvalModel(judge_client)
print(f"Judge model: {judge.get_model_name()}")Evaluations
DeepEvalModel is a thin adapter that wraps any AIMU ModelClient so it can act as the judge model for DeepEval metrics. This lets you use a local Ollama model, a HuggingFace model, or any cloud provider as your evaluator without leaving the AIMU interface.
| Section | Metric | What it measures |
|---|---|---|
| A | Setup | Create the judge adapter |
| B | GEval | Custom natural-language criteria (correctness, helpfulness, etc.) |
| C | Answer Relevancy | Whether the response directly addresses the input question |
| D | Faithfulness | Whether the response is grounded in retrieved context (RAG) |
| E | Batch Evaluation | Running multiple metrics across a full test suite |
Install the optional dependency before running: pip install -e '.[deepeval]'
A - Setup
Create a ModelClient and wrap it with DeepEvalModel. Any AIMU client can be the judge: swap in a cloud model for more reliable evaluations on complex tasks.
B - GEval (Custom Criteria)
GEval is DeepEval’s most flexible metric. You describe your evaluation criteria in plain language and the judge model assigns a score (0–1) with a written reason. Use it for any quality dimension: correctness, conciseness, tone, safety, etc.
evaluation_params tells GEval which fields from the test case to pass to the judge. Common choices are INPUT and ACTUAL_OUTPUT; add EXPECTED_OUTPUT when you have a reference answer.
from deepeval.metrics import GEval
from deepeval.test_case import LLMTestCase, LLMTestCaseParams
correctness = GEval(
name="Correctness",
criteria=(
"Determine whether the actual output is factually correct and directly answers "
"the input question. Penalise hallucinated or inaccurate facts."
),
evaluation_params=[LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT],
model=judge,
threshold=0.5,
)test_cases = [
LLMTestCase(
input="What is the capital of France?",
actual_output="The capital of France is Paris.",
),
LLMTestCase(
input="What is the capital of Australia?",
actual_output="The capital of Australia is Sydney.", # wrong, it's Canberra
),
LLMTestCase(
input="Who wrote Romeo and Juliet?",
actual_output="Romeo and Juliet was written by William Shakespeare.",
),
]
for tc in test_cases:
correctness.measure(tc)
status = "PASS" if correctness.passed else "FAIL"
print(f"[{status}] score={correctness.score:.2f}")
print(f" Q: {tc.input}")
print(f" A: {tc.actual_output}")
print(f" Reason: {correctness.reason}")
print()Add expected_output and LLMTestCaseParams.EXPECTED_OUTPUT when you have a reference answer. The judge can then compare against the gold standard directly.
correctness_with_ref = GEval(
name="Correctness (with reference)",
criteria="Does the actual output convey the same facts as the expected output?",
evaluation_params=[
LLMTestCaseParams.INPUT,
LLMTestCaseParams.ACTUAL_OUTPUT,
LLMTestCaseParams.EXPECTED_OUTPUT,
],
model=judge,
threshold=0.5,
)
tc = LLMTestCase(
input="Summarise the water cycle in one sentence.",
actual_output="Water evaporates from surfaces, condenses into clouds, and falls back as precipitation.",
expected_output="The water cycle involves evaporation, condensation, and precipitation.",
)
correctness_with_ref.measure(tc)
print(f"Score: {correctness_with_ref.score:.2f} Passed: {correctness_with_ref.passed}")
print(f"Reason: {correctness_with_ref.reason}")C - Answer Relevancy
AnswerRelevancyMetric checks whether the response actually addresses the question, useful for catching responses that are accurate but off-topic. It works by extracting claims from the output and classifying each one as relevant or irrelevant to the input.
from deepeval.metrics import AnswerRelevancyMetric
relevancy = AnswerRelevancyMetric(threshold=0.5, model=judge)
relevancy_cases = [
LLMTestCase(
input="How do I reverse a list in Python?",
actual_output=(
"You can reverse a list in Python using list.reverse() to reverse in place, "
"or list[::-1] to return a new reversed list."
),
),
LLMTestCase(
input="How do I reverse a list in Python?",
actual_output=(
"Python is a popular high-level programming language created by Guido van Rossum "
"and first released in 1991."
), # true, but irrelevant to the question
),
]
for tc in relevancy_cases:
relevancy.measure(tc)
status = "PASS" if relevancy.passed else "FAIL"
print(f"[{status}] score={relevancy.score:.2f}")
print(f" A: {tc.actual_output[:100]}")
print(f" Reason: {relevancy.reason}")
print()D - Faithfulness (RAG Grounding)
FaithfulnessMetric checks whether every claim in the response is supported by the retrieved context: the core hallucination-detection metric for RAG pipelines. Test cases require a retrieval_context field (list of retrieved chunks).
from deepeval.metrics import FaithfulnessMetric
faithfulness = FaithfulnessMetric(threshold=0.7, model=judge)
context = [
"The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. "
"It was designed by engineer Gustave Eiffel and completed in 1889 as the entrance arch "
"for the 1889 World's Fair. It stands 330 metres tall."
]
rag_cases = [
LLMTestCase(
input="When was the Eiffel Tower completed and how tall is it?",
actual_output="The Eiffel Tower was completed in 1889 and stands 330 metres tall.",
retrieval_context=context,
),
LLMTestCase(
input="When was the Eiffel Tower completed and how tall is it?",
actual_output=(
"The Eiffel Tower was completed in 1901 and is the tallest structure in Europe."
), # hallucinated date and unsupported claim
retrieval_context=context,
),
]
for tc in rag_cases:
faithfulness.measure(tc)
status = "PASS" if faithfulness.passed else "FAIL"
print(f"[{status}] score={faithfulness.score:.2f}")
print(f" A: {tc.actual_output}")
print(f" Reason: {faithfulness.reason}")
print()E - Batch Evaluation
Run multiple metrics across a list of test cases and collect results into a summary. This is the typical pattern for evaluating a model or pipeline against a labelled dataset before shipping.
batch_cases = [
LLMTestCase(
input="What is the boiling point of water at sea level?",
actual_output="Water boils at 100°C (212°F) at standard atmospheric pressure.",
retrieval_context=[
"Water boils at 100 degrees Celsius (212 degrees Fahrenheit) at standard "
"atmospheric pressure (sea level, 1 atm)."
],
),
LLMTestCase(
input="Who invented the telephone?",
actual_output="The telephone was invented by Alexander Graham Bell, who received the first patent for it in 1876.",
retrieval_context=[
"Alexander Graham Bell is widely credited with inventing the telephone and "
"received the first patent in 1876."
],
),
LLMTestCase(
input="What is the speed of light in a vacuum?",
actual_output="The speed of light in a vacuum is approximately 300,000 kilometres per second.",
retrieval_context=[
"The speed of light in a vacuum is exactly 299,792,458 metres per second, "
"commonly approximated as 300,000 km/s."
],
),
LLMTestCase(
input="What is the chemical formula for table salt?",
actual_output="Table salt is made of hydrogen and oxygen.", # wrong
retrieval_context=[
"Table salt is sodium chloride, with the chemical formula NaCl. It consists of sodium and chlorine ions."
],
),
]
batch_correctness = GEval(
name="Correctness",
criteria="Is the actual output factually correct and directly responsive to the question?",
evaluation_params=[LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT],
model=judge,
threshold=0.5,
)
batch_faithfulness = FaithfulnessMetric(threshold=0.7, model=judge)
metrics = [("Correctness", batch_correctness), ("Faithfulness", batch_faithfulness)]import pandas as pd
rows = []
for tc in batch_cases:
row = {
"question": tc.input,
"answer": tc.actual_output[:60] + "..." if len(tc.actual_output) > 60 else tc.actual_output,
}
for name, metric in metrics:
metric.measure(tc)
row[f"{name} score"] = round(metric.score, 2)
row[f"{name} pass"] = metric.passed
rows.append(row)
results_df = pd.DataFrame(rows)
print(results_df.to_string(index=False))
print()
for name, _ in metrics:
pass_rate = results_df[f"{name} pass"].mean()
mean_score = results_df[f"{name} score"].mean()
print(f"{name}: mean_score={mean_score:.2f} pass_rate={pass_rate:.0%}")