DeepEval Framework Setup
This skill builds a reusable LLM-as-judge evaluation harness around one or
more apps under test (a chatbot, a RAG pipeline, an AI agent — anything that
answers over HTTP). It is the generalized form of Chapter_19_DeepEval_Framework/03_DeepFramework,
which already evaluates three live targets. Treat that directory as the canonical
reference implementation; this skill tells you how to stand up the same thing for
a brand-new application.
When you're invoked, first establish three things
- What is the target? A chatbot, a RAG system, or an agent? How is it
reached — local FastAPI, a hosted URL, a function call? What's the request
shape and the response shape (JSON vs plain text)?
- What does "good" mean here? Which qualities matter — staying on topic,
grounding in retrieved context, no hallucination, no bias/toxicity, no PII
leak, citing sources, multi-turn memory?
- Which judge LLM? OpenAI (highest limits), Groq (fast, free tier is
rate-limited), or local Ollama (no key, fully offline).
Don't guess these. If the user hasn't said, ask — the answers decide the
targets, the metric registry rows, and the datasets.
The mental model
app under test ──HTTP──▶ target client ──▶ LLMTestCase ──▶ metric
│
judge LLM ◀──────┘
│
score + pass/fail + reason
The framework never inspects the model inside the app — it treats the app as a
black box reached over HTTP, wraps each response in a DeepEval LLMTestCase,
and hands it to a metric. The metric prompts the judge LLM (a different,
trusted model) which returns a numeric score and a written reason. This
separation is the whole point: the thing being graded and the grader are
different models, so the grade is independent.
Directory layout to create
Mirror this structure. It's deliberately flat so the dashboard and the pytest
suites import the same modules — never duplicate metric logic between them.
<framework>/
├── .env # keys + JUDGE_PROVIDER (gitignored — never commit)
├── requirements.txt # pinned (see Versions below)
├── conftest.py # loads .env, fixtures, liveness skip, run capture
├── pytest.ini # markers
├── llm_providers/
│ ├── base.py # CompatibleJudge(DeepEvalBaseLLM)
│ └── factory.py # get_judge() reads JUDGE_PROVIDER
├── targets/
│ └── <app>.py # HTTP client per app under test
├── datasets/
│ └── <app>_goldens.py # golden Q&A + safety/PII probes
├── dashboard/
│ ├── app.py # FastAPI on :8203 (interactive UI + REST)
│ ├── registry.py # the single source of truth: MetricDef rows
│ ├── runner.py # run one registry metric end-to-end → JSON
│ └── runs_store.py # local run-history capture
└── tests/
└── <app>/test_NN_*.py # one pytest file per metric
The full, working code for every one of these files lives in
references/templates.md. Read it when you're writing the files — don't
reconstruct them from memory. Read references/metrics-catalog.md for the
complete list of metrics, their thresholds, and their score directions.
Setup workflow
Follow these in order. Each step is small on purpose — verify before moving on.
Scaffold + venv. Create the directory tree, a requirements.txt with the
pinned versions below, then python3 -m venv venv && venv/bin/pip install -r requirements.txt. Verify with venv/bin/python -c "import deepeval, instructor, openai; print(deepeval.__version__)".
Judge factory (llm_providers/). Copy CompatibleJudge and the factory
from references/templates.md. One class serves all three providers because
OpenAI, Groq, and Ollama all speak the OpenAI-compatible wire protocol;
instructor handles structured output uniformly. Verify:
JUDGE_PROVIDER=openai venv/bin/python -c "from llm_providers import get_judge; print(get_judge().get_model_name())".
Target client(s) (targets/). One small requests-based class per app —
a health()/is_alive() and a chat() that returns a dataclass. Match the
app's real contract: JSON body vs form, JSON response vs plain text, whether
it takes conversation history. See the two contrasting examples in the
templates (local JSON chatbot vs plain-text hosted bot).
Golden dataset(s) (datasets/). A dataclass per golden carrying input,
expected_output, context (ground-truth facts), and categories. Plus a
flat list of adversarial SAFETY_PROMPTS. Ground every expected_output and
context in the app's real source of truth — a golden that cites facts the
app doesn't have produces a misleading score.
Metric registry (dashboard/registry.py). This is the heart. One
MetricDef row per (metric × target). Each row names a factory(judge, threshold), a sample_kind (which dataset the runner pulls from), a
requires list (which LLMTestCase fields the metric needs), and a
higher_is_better flag that sets the threshold direction. Adding a metric =
adding a row; it appears in the dashboard instantly.
Runner (dashboard/runner.py). Maps a sample_kind to a dataset, calls
the target, builds the right LLMTestCase, runs the metric, returns a
JSON-friendly dict. Copy it as-is; extend only the sample_kind switch when
you add a new kind.
conftest + pytest.ini. conftest.py loads .env, exposes
judge/target/golden fixtures, and auto-skips a suite when its target app is
down (needs_<app> markers). Register the markers in pytest.ini so pytest
doesn't warn.
Dashboard (dashboard/app.py). FastAPI on :8203 that renders straight
from the registry — click a metric, see live pass/fail/score/reason, switch
judge provider on the fly. Start it with uvicorn dashboard.app:app --port 8203.
pytest suites (tests/<app>/). One file per metric, using
assert_test(test_case, [metric]) so deepeval test run captures each case
(needed for the Confident AI cloud dashboard). Plain metric.measure() +
assert is_successful() works for local pytest but is NOT captured by the CLI.
Verify end-to-end. Start the target app(s), then run one metric through
the dashboard (curl -X POST localhost:8203/api/run -d '{"metric_id":"...","sample_idx":0}')
and one pytest suite. Confirm a real score comes back before declaring done.
Versions — pin these
DeepEval's API and packaging move fast; pin to avoid surprises.
| Package |
Version |
Why |
deepeval |
3.9.9 |
4.0.6 (latest) ships a broken deepeval test run CLI — its code does from deepeval.deepeval.config.settings import ..., a typo; there is no nested deepeval/deepeval/ package. The library works on 4.x, only the CLI crashes. 3.9.9 is the last release whose CLI works out of the box and still has every metric (PIILeakageMetric, KnowledgeRetentionMetric, ConversationCompletenessMetric). |
openai |
>=2.0 |
judge transport (also used for Groq/Ollama via base_url) |
groq |
>=1.0 |
optional, for Groq-hosted apps under test |
instructor |
>=1.6 |
structured output for judge prompts |
fastapi / uvicorn / jinja2 |
0.115 / 0.32 / 3.1 |
the dashboard |
pytest |
>=8 |
the suites |
Judge models per provider: openai → gpt-4o-mini, groq → openai/gpt-oss-120b,
ollama → gpt-oss:20b. Override with JUDGE_MODEL_OPENAI / _GROQ / _OLLAMA.
Gotchas worth knowing up front
These come from actually running the framework — flag them before the user hits them.
HallucinationMetric needs a non-empty context. It throws on an empty
list. Out-of-scope / refusal goldens often have no natural context — give them
a scope-policy line, or tag them to skip hallucination.
.env is not auto-loaded by the apps under test. The eval framework's
conftest.py / dashboard load it, but the chatbot/RAG apps read raw
os.getenv — export the key (or set -a; source .env; set +a) when launching them.
- Groq's free tier caps
gpt-oss-120b at 8000 TPM. deepeval test run fans
out judge calls concurrently and blows past it (429). For large or CLI runs,
use the OpenAI judge (JUDGE_PROVIDER=openai, gpt-4o-mini).
- Score direction is inverted for safety metrics. Higher is better for
quality (threshold = floor); lower is better for hallucination/bias/toxicity/PII
(threshold = ceiling). Don't hand-roll the comparison —
metric.is_successful()
already handles the inversion.
deepeval test run only captures assert_test() cases. A manual
measure() loop runs fine locally but the CLI reports "No test cases found" and
pushes nothing to Confident AI.
- Different apps have different contracts. A local FastAPI bot may return JSON
{reply, model, mode}; a hosted bot may return plain text with a visitorId.
The target client absorbs that difference so the rest of the framework doesn't care.
Adding a new target (a new app to evaluate)
Common request: "evaluate my agent the same way." Steps:
- Add
targets/<app>.py — a client matching that app's HTTP contract.
- Add
datasets/<app>_goldens.py — goldens grounded in that app's truth.
- Add
<app> fixtures + a needs_<app> liveness skip in conftest.py, and the
markers in pytest.ini.
- Add
MetricDef rows with target="<app>" to registry.py (reuse the metric
factories — only the target + dataset change).
- Extend
runner.py's _call_target to dispatch to the new client.
- Add
tests/<app>/test_NN_*.py suites.
Because the registry drives the dashboard, the new target's metrics become
clickable buttons with no UI changes.
Adding a new metric
- Use a built-in DeepEval metric, or define a
GEval with a plain-English
rubric (best for bespoke criteria like "cites a real source filename").
- Write a one-line
factory(judge, threshold) in registry.py.
- Append a
MetricDef row (set higher_is_better and sample_kind).
- Optionally mirror it as a
tests/<app>/test_NN_*.py using assert_test.
That's it — see references/templates.md for the exact code shapes.
1---2name: deepeval-framework-setup3description: Set up a DeepEval LLM-as-judge evaluation framework from scratch for any chatbot, RAG pipeline, AI agent, or LLM-backed app under test — the same architecture used in Chapter 19 (ShopSphere chatbot, RAG Explorer, and the live BrowserBash bot). Use this skill WHENEVER the user wants to "evaluate", "test", "score", "benchmark", "add metrics to", "measure quality of", or "QA" a chatbot / RAG / agent / LLM app, or asks to "set up DeepEval", "build an eval harness", "judge an LLM", "add a new eval target", "add a metric", or replicate the Chapter 19 framework for a new application — even if they don't say the word "DeepEval". Covers the judge factory (OpenAI/Groq/Ollama), HTTP target clients, the metric registry, golden datasets, the FastAPI dashboard, the pytest suites, version pins, and the known gotchas.4---56# DeepEval Framework Setup78This skill builds a reusable **LLM-as-judge evaluation harness** around one or9more *apps under test* (a chatbot, a RAG pipeline, an AI agent — anything that10answers over HTTP). It is the generalized form of `Chapter_19_DeepEval_Framework/03_DeepFramework`,11which already evaluates three live targets. Treat that directory as the canonical12reference implementation; this skill tells you how to stand up the same thing for13a brand-new application.1415## When you're invoked, first establish three things16171. **What is the target?** A chatbot, a RAG system, or an agent? How is it18 reached — local FastAPI, a hosted URL, a function call? What's the request19 shape and the response shape (JSON vs plain text)?202. **What does "good" mean here?** Which qualities matter — staying on topic,21 grounding in retrieved context, no hallucination, no bias/toxicity, no PII22 leak, citing sources, multi-turn memory?233. **Which judge LLM?** OpenAI (highest limits), Groq (fast, free tier is24 rate-limited), or local Ollama (no key, fully offline).2526Don't guess these. If the user hasn't said, ask — the answers decide the27targets, the metric registry rows, and the datasets.2829## The mental model3031```32app under test ──HTTP──▶ target client ──▶ LLMTestCase ──▶ metric33 │34 judge LLM ◀──────┘35 │36 score + pass/fail + reason37```3839The framework never inspects the model inside the app — it treats the app as a40black box reached over HTTP, wraps each response in a DeepEval `LLMTestCase`,41and hands it to a metric. The metric prompts the **judge LLM** (a different,42trusted model) which returns a numeric score and a written reason. This43separation is the whole point: the thing being graded and the grader are44different models, so the grade is independent.4546## Directory layout to create4748Mirror this structure. It's deliberately flat so the dashboard and the pytest49suites import the *same* modules — never duplicate metric logic between them.5051```52<framework>/53├── .env # keys + JUDGE_PROVIDER (gitignored — never commit)54├── requirements.txt # pinned (see Versions below)55├── conftest.py # loads .env, fixtures, liveness skip, run capture56├── pytest.ini # markers57├── llm_providers/58│ ├── base.py # CompatibleJudge(DeepEvalBaseLLM)59│ └── factory.py # get_judge() reads JUDGE_PROVIDER60├── targets/61│ └── <app>.py # HTTP client per app under test62├── datasets/63│ └── <app>_goldens.py # golden Q&A + safety/PII probes64├── dashboard/65│ ├── app.py # FastAPI on :8203 (interactive UI + REST)66│ ├── registry.py # the single source of truth: MetricDef rows67│ ├── runner.py # run one registry metric end-to-end → JSON68│ └── runs_store.py # local run-history capture69└── tests/70 └── <app>/test_NN_*.py # one pytest file per metric71```7273The full, working code for every one of these files lives in74`references/templates.md`. Read it when you're writing the files — don't75reconstruct them from memory. Read `references/metrics-catalog.md` for the76complete list of metrics, their thresholds, and their score directions.7778## Setup workflow7980Follow these in order. Each step is small on purpose — verify before moving on.81821. **Scaffold + venv.** Create the directory tree, a `requirements.txt` with the83 pinned versions below, then `python3 -m venv venv && venv/bin/pip install -r84 requirements.txt`. Verify with `venv/bin/python -c "import deepeval,85 instructor, openai; print(deepeval.__version__)"`.86872. **Judge factory** (`llm_providers/`). Copy `CompatibleJudge` and the factory88 from `references/templates.md`. One class serves all three providers because89 OpenAI, Groq, and Ollama all speak the OpenAI-compatible wire protocol;90 `instructor` handles structured output uniformly. Verify:91 `JUDGE_PROVIDER=openai venv/bin/python -c "from llm_providers import get_judge; print(get_judge().get_model_name())"`.92933. **Target client(s)** (`targets/`). One small `requests`-based class per app —94 a `health()`/`is_alive()` and a `chat()` that returns a dataclass. Match the95 app's real contract: JSON body vs form, JSON response vs plain text, whether96 it takes conversation `history`. See the two contrasting examples in the97 templates (local JSON chatbot vs plain-text hosted bot).98994. **Golden dataset(s)** (`datasets/`). A dataclass per golden carrying `input`,100 `expected_output`, `context` (ground-truth facts), and `categories`. Plus a101 flat list of adversarial `SAFETY_PROMPTS`. Ground every `expected_output` and102 `context` in the app's real source of truth — a golden that cites facts the103 app doesn't have produces a misleading score.1041055. **Metric registry** (`dashboard/registry.py`). This is the heart. One106 `MetricDef` row per (metric × target). Each row names a `factory(judge,107 threshold)`, a `sample_kind` (which dataset the runner pulls from), a108 `requires` list (which `LLMTestCase` fields the metric needs), and a109 `higher_is_better` flag that sets the threshold direction. Adding a metric =110 adding a row; it appears in the dashboard instantly.1111126. **Runner** (`dashboard/runner.py`). Maps a `sample_kind` to a dataset, calls113 the target, builds the right `LLMTestCase`, runs the metric, returns a114 JSON-friendly dict. Copy it as-is; extend only the `sample_kind` switch when115 you add a new kind.1161177. **conftest + pytest.ini.** `conftest.py` loads `.env`, exposes118 `judge`/target/golden fixtures, and auto-skips a suite when its target app is119 down (`needs_<app>` markers). Register the markers in `pytest.ini` so pytest120 doesn't warn.1211228. **Dashboard** (`dashboard/app.py`). FastAPI on `:8203` that renders straight123 from the registry — click a metric, see live pass/fail/score/reason, switch124 judge provider on the fly. Start it with `uvicorn dashboard.app:app --port 8203`.1251269. **pytest suites** (`tests/<app>/`). One file per metric, using127 `assert_test(test_case, [metric])` so `deepeval test run` captures each case128 (needed for the Confident AI cloud dashboard). Plain `metric.measure()` +129 `assert is_successful()` works for local pytest but is NOT captured by the CLI.13013110. **Verify end-to-end.** Start the target app(s), then run one metric through132 the dashboard (`curl -X POST localhost:8203/api/run -d '{"metric_id":"...","sample_idx":0}'`)133 and one pytest suite. Confirm a real score comes back before declaring done.134135## Versions — pin these136137DeepEval's API and packaging move fast; pin to avoid surprises.138139| Package | Version | Why |140|---------|---------|-----|141| `deepeval` | **`3.9.9`** | `4.0.6` (latest) ships a **broken `deepeval test run` CLI** — its code does `from deepeval.deepeval.config.settings import ...`, a typo; there is no nested `deepeval/deepeval/` package. The *library* works on 4.x, only the CLI crashes. `3.9.9` is the last release whose CLI works out of the box **and** still has every metric (`PIILeakageMetric`, `KnowledgeRetentionMetric`, `ConversationCompletenessMetric`). |142| `openai` | `>=2.0` | judge transport (also used for Groq/Ollama via base_url) |143| `groq` | `>=1.0` | optional, for Groq-hosted apps under test |144| `instructor` | `>=1.6` | structured output for judge prompts |145| `fastapi` / `uvicorn` / `jinja2` | `0.115 / 0.32 / 3.1` | the dashboard |146| `pytest` | `>=8` | the suites |147148Judge models per provider: `openai` → `gpt-4o-mini`, `groq` → `openai/gpt-oss-120b`,149`ollama` → `gpt-oss:20b`. Override with `JUDGE_MODEL_OPENAI` / `_GROQ` / `_OLLAMA`.150151## Gotchas worth knowing up front152153These come from actually running the framework — flag them before the user hits them.154155- **`HallucinationMetric` needs a non-empty `context`.** It throws on an empty156 list. Out-of-scope / refusal goldens often have no natural context — give them157 a scope-policy line, or tag them to skip hallucination.158- **`.env` is not auto-loaded by the apps under test.** The eval framework's159 `conftest.py` / dashboard load it, but the chatbot/RAG apps read raw160 `os.getenv` — export the key (or `set -a; source .env; set +a`) when launching them.161- **Groq's free tier caps `gpt-oss-120b` at 8000 TPM.** `deepeval test run` fans162 out judge calls concurrently and blows past it (429). For large or CLI runs,163 use the OpenAI judge (`JUDGE_PROVIDER=openai`, `gpt-4o-mini`).164- **Score direction is inverted for safety metrics.** Higher is better for165 quality (threshold = floor); lower is better for hallucination/bias/toxicity/PII166 (threshold = ceiling). Don't hand-roll the comparison — `metric.is_successful()`167 already handles the inversion.168- **`deepeval test run` only captures `assert_test()` cases.** A manual169 `measure()` loop runs fine locally but the CLI reports "No test cases found" and170 pushes nothing to Confident AI.171- **Different apps have different contracts.** A local FastAPI bot may return JSON172 `{reply, model, mode}`; a hosted bot may return plain text with a `visitorId`.173 The target client absorbs that difference so the rest of the framework doesn't care.174175## Adding a new target (a new app to evaluate)176177Common request: "evaluate my agent the same way." Steps:1781791. Add `targets/<app>.py` — a client matching that app's HTTP contract.1802. Add `datasets/<app>_goldens.py` — goldens grounded in that app's truth.1813. Add `<app>` fixtures + a `needs_<app>` liveness skip in `conftest.py`, and the182 markers in `pytest.ini`.1834. Add `MetricDef` rows with `target="<app>"` to `registry.py` (reuse the metric184 factories — only the target + dataset change).1855. Extend `runner.py`'s `_call_target` to dispatch to the new client.1866. Add `tests/<app>/test_NN_*.py` suites.187188Because the registry drives the dashboard, the new target's metrics become189clickable buttons with no UI changes.190191## Adding a new metric1921931. Use a built-in DeepEval metric, or define a `GEval` with a plain-English194 rubric (best for bespoke criteria like "cites a real source filename").1952. Write a one-line `factory(judge, threshold)` in `registry.py`.1963. Append a `MetricDef` row (set `higher_is_better` and `sample_kind`).1974. Optionally mirror it as a `tests/<app>/test_NN_*.py` using `assert_test`.198199That's it — see `references/templates.md` for the exact code shapes.