How OpenGrade works¶
This page connects the pieces: what runs where, the objects you work with, and the order you use them in. Each step links to the page with the details. If you have not run OpenGrade yet, do the 15-minute getting started first.
The loop¶
trace your app ──▶ build a test set ──▶ run an experiment ──▶ compare with the baseline ──▶ CI gate
▲ │
└────────────── production traces, scores and alerts ◀────────── ship ◀───────────────┘
- Trace your app so every request is recorded as a span tree.
- Build a test set (gold questions with expected answers and documents), from a file or from production traffic.
- Run an experiment: your code answers every item of the test set and the server scores the answers.
- Compare the experiment with a baseline (the last accepted run).
- Gate the pull request on a YAML policy: CI fails when quality drops.
- Ship and monitor: production traces are scored, alerts fire on drift, bad answers become new test cases.
What runs where¶
| Piece | Package | What it does |
|---|---|---|
| SDK | opengrade-ai (import opengrade) |
Runs in your app and in your eval scripts: tracing, evaluate / sweep, local metrics, production scores and feedback |
| Server + UI | opengrade-ai-server (installed by opengrade-ai[ui]) |
Stores traces, datasets and experiments; runs the server metrics and the LLM judges; serves the dashboard and the REST API |
| CLI | opengrade (ships with the SDK, needs the server package) |
opengrade ui (local server), opengrade evaluate / check (CI gate), opengrade keys |
Two consequences that are easy to miss:
- LLM judges run on the server, not in your process. Their model and API key are server
settings (
LLM_MODEL,LLM_API_BASE,LLM_API_KEYin the.envnext toopengrade ui, or inbackend/.envwith Docker Compose). Without a reachable judge model, traces and deterministic metrics still work and judge metrics show as unavailable. See LLM judges. - The server is a database. Baselines (aliases), test sets and production data live there.
opengrade uiuses a SQLite file (./opengrade.db); for a team or CI use a persistent server on PostgreSQL (Docker Compose or--backend-store-uri).opengrade evaluatewithout a server URL starts a throwaway in-process server: fine for an absolute gate, but it cannot remember a baseline between runs.
Objects¶
| Term | Meaning |
|---|---|
| Project | Workspace that holds everything else. Opengrade(project="name") creates it on first use |
| Trace | Span tree of one request, sent over OTLP to /v1/traces |
| Dataset (test set) | Versioned list of items: input, expected_output, metadata (e.g. expected_doc_ids, must_contain, critical). Called test set in the UI |
| Task type | Shape of the items and default metrics: rag_qa (default), classification, agent_tools |
| Metric (evaluator) | Scores one answer, 0..1 with PASS / FAIL / SKIPPED / ERROR. Deterministic (hit_at_k, must_contain, latency, …), LLM judge (groundedness, correctness, …), custom judge (LlmJudge) or local Python (@opengrade.metric) |
| Metrics set | Named, versioned list of metrics with thresholds. Each project has a default one. Older pages and API routes call it metrics pack |
| Experiment (run) | One pass of your code over a dataset, scored by a metrics set. Records params, version and git commit |
| Baseline / alias | The experiment to compare with. An alias (alias:main) points to the last accepted run and moves on --promote |
| Policy | YAML rules (opengrade.yaml) that turn an experiment and its baseline into pass / fail for CI |
| Live interaction | A production request recorded with og.init_tracing(live=True) and scored by judges without gold. Can be promoted into a dataset |
Step by step¶
1. Run a server¶
| Situation | How |
|---|---|
| Trying it out, local development | uv add 'opengrade-ai[ui]' and uv run opengrade ui (SQLite, no Docker) |
| Team, CI baselines, production traffic | docker compose up (PostgreSQL), or opengrade ui --backend-store-uri postgresql+asyncpg://… |
| Server reachable beyond localhost | set OPENGRADE_API_KEY, create per-client keys, put TLS in front: Security and API keys |
Apps and scripts find the server through Settings(tracking_uri=...) /
OPENGRADE_TRACKING_URI (default http://localhost:8000), and send a key from
OPENGRADE_API_KEY / Settings(api_key=...) when the server requires one.
2. Trace your app¶
from opengrade import Opengrade
og = Opengrade(project="support-bot") # created if missing
@og.trace # OpenAI, Anthropic, LangChain, LlamaIndex, Bedrock calls become child spans
def answer(question: str) -> str:
docs = retriever.search(question)
og.set_retrieval_documents(docs) # needed by retrieval metrics and RAG judges
return llm.generate(question, docs)
answer("How many PTO days do I get?")
Install the extra for your provider (opengrade-ai[openai], [anthropic], …, or [ai] for all).
Details: Python SDK → Quickstart, spans,
users, sessions, tags.
The same function is used in production and in experiments: write it once, evaluate it as it ships.
3. Build a test set¶
A test set is a file (CSV, JSON, JSONL), a list of dicts, a DataFrame, or a dataset already stored on the server. One row per test case:
{"input": "How many PTO days do I get?", "expected_output": "20 days per year.", "expected_doc_ids": ["pto"], "category": "hr"}
{"input": "How often must passwords be rotated?", "expected_output": "Every 90 days.", "expected_doc_ids": ["security"], "must_contain": ["90 days"], "critical": true}
{"input": "What is the CEO's salary?", "answerable": false}
| Column | Used by |
|---|---|
input (or question) |
required: what your task receives as item.input |
expected_output (or expected_answer) |
reference answer: correctness, context_recall |
expected_doc_ids |
gold documents: hit_at_k, recall_at_k, mrr |
must_contain |
required phrases: must_contain |
answerable: false / expected_refusal: true |
the assistant should decline: refusal (no gold answer or documents needed) |
any other column (category, critical, …) |
stored in metadata: grouping (group_by, groups), must-pass cases (critical) |
When you do not pass task_type, it is inferred from the rows: rag_qa when they carry
expected_doc_ids (then every answerable row needs expected_output and expected_doc_ids),
rag_chat for chat-shaped inputs, otherwise generic (plain input → output). Datasets are versioned: once a dataset has been used by an experiment,
changing it creates a new version, so old experiments stay comparable.
Grow the test set from production: a failing live interaction can be promoted into a dataset
with a reviewer-written answer (UI: Live page; SDK: client.live_runs.promote, see
Live runs → test set).
How many test cases? Small sets cannot detect small drops: with 20 cases the gate detects only drops of about 0.28, with 100 about 0.07. See statistical power.
4. Choose metrics¶
| You want to check | Metrics | Needs |
|---|---|---|
| The retriever finds the right documents | hit_at_k, recall_at_k, mrr |
expected_doc_ids, documents from set_retrieval_documents |
| The answer is supported by the documents | groundedness, citation_accuracy |
LLM judge, documents with text |
| The answer is right | correctness, must_contain |
expected_output / must_contain |
| The answer addresses the question | answer_relevance |
LLM judge |
| It declines when it should | refusal |
answerable / expected_refusal |
| No personal data, toxicity or prompt injection | pii, toxicity, prompt_injection |
LLM judge; no gold needed |
| Speed and cost | latency, cost, token_usage |
tracing on (og.init_tracing()) for tokens and cost |
| Exact labels, formats, tool calls | exact_match, contains, regex, json_schema, tool_call_success |
set as a server evaluator in a metrics set |
| Something specific to your app | @opengrade.metric (Python) or opengrade.LlmJudge (criteria in plain language) |
— |
All metrics, judge configuration and scoring rules: Metrics and judges. Writing your own: Custom evaluators.
5. Run an experiment¶
result = og.evaluate(
dataset="gold.jsonl",
task=lambda item: answer(item.input),
metrics=["hit_at_k", "groundedness", "correctness", "latency"],
name="prompt-v2",
params={"model": "gemma4", "prompt": "v2"},
)
print(result.summary(), result.url)
- Compare several configurations (models, prompts,
top_k, chunk sizes) in one call withog.sweep(grid=...). - Test only the retriever, without generating answers, with
og.evaluate_retrieval. - Reduce LLM noise with
repeats=3; spot a weak category withgroup_by="category".
Details: Python SDK → Experiments. The experiment appears under Experiments in the UI, with per-item outputs, scores and judge explanations.
6. Compare with a baseline¶
Pass baseline= to evaluate (an experiment name, an id, or alias:main). The comparison is
paired item by item with a bootstrap confidence interval, so it tells a real regression from
noise. In the UI open the experiment and Compare; the CLI prints the same table.
An alias is how a baseline moves forward: alias:main points to the last run accepted on
main, and --promote main moves it when the gate passes. See
Moving baseline.
7. Gate the pull request¶
Put the experiment and the rules in one file and run it in CI:
# opengrade.yaml
evaluate:
task: rag_app:answer_item # module:function taking a dataset item
dataset: tests/gold.jsonl
metrics: [hit_at_k, must_contain, correctness, latency]
baseline: alias:main
hit_at_k: { min: 0.8 }
latency: { p95_max_ms: 2000 }
regression: { max_delta: -0.05, max_new_failures: 0 }
critical: { by: critical }
# pull request: must be compared with main
opengrade evaluate --config opengrade.yaml --require-baseline --report-md "$GITHUB_STEP_SUMMARY"
# push to main: same run, then move the baseline when it passes
opengrade evaluate --config opengrade.yaml --promote main
Exit code 0 passes, 1 is a failed gate, 2 a configuration error, 3 an infrastructure
error. Baselines need a persistent server (api_base_url in the YAML or --base-url) and an
admin API key in CI when the server is protected.
Every rule: Policy reference. Full workflows:
examples/ci_regression_gate (no LLM),
examples/llm_regression_gate (real LLM and judge).
8. Monitor production¶
live=True: each top-level traced request becomes a live interaction, and a sample of them is scored by the gold-less judges (groundedness,answer_relevance, custom judges withlive=True).@og.trace(metrics=[...])runs your own metrics off the request path;og.log_score/og.log_feedback(orcurrent().log_scoreinside a traced call) attach scores and thumbs up/down.version/environmentlet Production → Health compare one deploy with the previous one.- Alert rules (UI or
client.alerts.create) notify Slack or a webhook on thresholds, drift or anomalies.
Details: Live runs, production scores and feedback, alerts. What the server stores (prompts, answers, document text) and how to turn it off: content capture.
9. Close the loop¶
Review failing live interactions (UI: Live, filter Failing), write the correct answer, and promote them into the test set. The next pull request is gated on them too.
Where to go next¶
- Runnable, offline walkthrough of steps 2–8:
examples/sdk_tour - Every SDK function: Python SDK and the API reference
- Every CLI flag and policy rule: CLI and CI