Quickstart: Eval Runs, Prompt Versioning, and Test View Dashboard¶
Feature: 031-eval-runs-dashboard Audience: A developer evaluating whether this feature works end-to-end after Phase 2 implementation completes.
This quickstart is a hand-runnable recipe. It doubles as the "golden path" integration test script.
0. Prerequisites¶
# From a clean clone
make init
source .venv/bin/activate
holodeck --version
To exercise the dashboard, install the optional extra:
uv add 'holodeck-ai[dashboard]'
# or: pip install 'holodeck-ai[dashboard]'
1. Create a minimal agent with a frontmatter-annotated prompt¶
mkdir -p ./demo && cd ./demo
demo/instructions.md:
---
version: "1.0"
author: "[email protected]"
description: "Initial support prompt"
tags: [support, v1]
---
You are a helpful customer support agent. Always answer in one paragraph.
demo/agent.yaml:
name: support-demo
model:
provider: ollama
name: gpt-oss:20b
temperature: 0.2
instructions:
file: instructions.md
test_cases:
- name: "Refund question"
input: "Do you have a money-back guarantee?"
ground_truth: "We offer a 30-day money-back guarantee."
evaluations:
metrics:
- type: standard
metric: bleu
threshold: 0.1
2. Run the tests — persistence kicks in automatically¶
holodeck test agent.yaml
Expected console output (abbreviated):
✓ Refund question ... passed
1/1 tests passed
EvalRun persisted: results/support-demo/2026-04-18T14-22-09.812Z.json
Note: run files are intended to be committed (per spec Assumption 5) so experiment history stays diffable and reviewable alongside code changes. If your team prefers to keep runs local-only, add
results/to.gitignore— HoloDeck itself does not modify your.gitignore.
Verify the artifact:
ls results/support-demo/
# → 2026-04-18T14-22-09.812Z.json
python - <<'PY'
from holodeck.models.eval_run import EvalRun
import pathlib, json
p = next(pathlib.Path("results/support-demo").glob("*.json"))
run = EvalRun.model_validate_json(p.read_text())
print("version:", run.metadata.prompt_version.version)
print("author: ", run.metadata.prompt_version.author)
print("tags: ", run.metadata.prompt_version.tags)
print("source: ", run.metadata.prompt_version.source)
print("model: ", run.metadata.agent_config.model.name)
print("tests: ", run.report.summary.total_tests)
PY
Expected:
version: 1.0
author: [email protected]
tags: ['support', 'v1']
source: file
model: gpt-oss:20b
tests: 1
3. Verify auto-version when no manual version: is present¶
Edit instructions.md, remove only the version: "1.0" line (keep the rest of the frontmatter). Re-run:
holodeck test agent.yaml
Inspect the new file:
python -c "
from holodeck.models.eval_run import EvalRun
import pathlib
files = sorted(pathlib.Path('results/support-demo').glob('*.json'))
latest = files[-1]
run = EvalRun.model_validate_json(latest.read_text())
print(run.metadata.prompt_version.version) # e.g., auto-3f9a1c2b
"
Re-run holodeck test agent.yaml without editing the prompt body. The auto-version in the new file MUST match the previous auto-version — this is SC-003.
Now change one character of the prompt body (e.g., one paragraph → two paragraphs). Re-run. The auto-version MUST differ — this is SC-004.
4. Verify the snapshot is frozen (Story 3)¶
- Copy the latest run file aside:
cp results/support-demo/*.json /tmp/pinned_run.json - Edit
agent.yaml: changetemperature: 0.2→temperature: 0.9. - Re-load
/tmp/pinned_run.json:python -c " from holodeck.models.eval_run import EvalRun r = EvalRun.model_validate_json(open('/tmp/pinned_run.json').read()) print(r.metadata.agent_config.model.temperature) # MUST print 0.2 "
5. Verify secret redaction¶
Set a fake API key and run:
OPENAI_API_KEY=sk-not-a-real-key holodeck test agent.yaml
Inspect the newest run file:
python -c "
import json, pathlib
p = sorted(pathlib.Path('results/support-demo').glob('*.json'))[-1]
doc = json.loads(p.read_text())
ak = doc['metadata']['agent_config']['model'].get('api_key')
print(repr(ak)) # '***' or absent, NEVER 'sk-not-a-real-key'
"
6. Launch the dashboard (requires [dashboard] extra)¶
holodeck test view agent.yaml
Expected console:
Warning: Streamlit binds to 0.0.0.0 by default. results/ may contain
sensitive conversation/tool data — firewall this port on shared infrastructure.
You can now view your Streamlit app in your browser.
Local URL: http://localhost:8501
A browser tab opens showing:
- Summary page:
- Sortable table: timestamp, pass_rate, total_tests, prompt_version, model_name.
- Line chart: pass rate over time.
- Line chart: per-metric average score over time.
- Three breakdown panels: standard (bleu/rouge/meteor), rag (5 sub-metrics), geval (per custom name).
- Sidebar filters: date range, prompt-version multi-select, model-name multi-select, pass-rate slider, tag multi-select.
- Changing filters updates the URL query string; copy-pasting the URL into a new tab reproduces the filtered view.
- Explorer page (linked from the run table):
- Click a run → list of test cases with pass/fail badges.
- Click a test case → detail panel with agent config snapshot, chat-style conversation, tool-call panels (collapsed when large), expected-tools check/cross, metric results with score/threshold/reasoning.
Ctrl+C in the terminal terminates the server cleanly.
7. Verify the missing-extra install hint¶
Without the dashboard extra installed:
uv remove streamlit # simulate
holodeck test view agent.yaml
Expected (exit code 2):
Dashboard not installed. Install the optional extra:
uv add 'holodeck-ai[dashboard]' # or: pip install 'holodeck-ai[dashboard]'
No Python traceback (SC-007).
8. Verify empty state¶
In a fresh directory with no prior runs:
mkdir /tmp/empty-demo && cd /tmp/empty-demo
# copy agent.yaml + instructions.md, do NOT run `holodeck test`
holodeck test view agent.yaml
The dashboard loads with an empty-state panel:
No runs found yet — execute
holodeck testto generate one.
No traceback.
10. Verify tool-call capture (dashboard Explorer prerequisite)¶
The dashboard's Explorer view needs {name, args, result, bytes} per tool call to render the ToolCall panel. Confirm tool_invocations is populated end-to-end.
Add a trivial function tool to demo/agent.yaml (or use any fixture with an MCP/function tool) and a test case that triggers it. Then:
holodeck test agent.yaml
python - <<'PY'
import pathlib
from holodeck.models.eval_run import EvalRun
p = sorted(pathlib.Path("results/support-demo").glob("*.json"))[-1]
run = EvalRun.model_validate_json(p.read_text())
tc = run.report.results[0]
print("tool_invocations:", len(tc.tool_invocations))
for inv in tc.tool_invocations:
print(f" - {inv.name}({inv.args}) -> {inv.bytes} bytes")
print("token_usage:", tc.token_usage)
print("metric kinds:", {m.kind for m in tc.metric_results})
PY
Expected:
- tool_invocations is non-empty when the test triggered at least one tool.
- Each invocation has a non-empty name, a dict args (possibly empty), a result, and bytes == len(json.dumps(result, default=str)).
- token_usage is a TokenUsage object (not None) when the backend reported it.
- Every MetricResult has kind ∈ {"standard","rag","geval"}.
If tool_invocations is empty but tool_calls (names-only) is not, the pairing layer (see research.md R8) didn't run — report as a bug. Back-compat readers will fall through to tool_calls.
11. Success checklist¶
| Criterion | Verified by step |
|---|---|
| SC-001: Locate persisted file in <10 s | §2 |
| SC-002: Reconstruct agent config from JSON alone | §4 |
| SC-003: Same body → same auto-version | §3 |
| SC-004: One-char edit → different auto-version | §3 |
| SC-005: Summary renders for ≥50 runs | Populate results/ with 50 files and re-run §6 |
| SC-006: Drill to test case in ≤3 clicks | §6 (Explorer) |
| SC-007: Clean install hint on missing extra | §7 |
| SC-008: <200 ms persistence overhead | Micro-benchmark: time holodeck test with 1 test case, compare before/after |
SC-009: --output output byte-equivalent |
diff against pre-feature output on the same config |
| SC-010: <5 s P95 load for 1000 runs | Populate 1000 synthetic runs and time Summary first-render |
| SC-011: Tool-call args/result captured | §10 |
| SC-012: Token usage persisted for cost computation | §10 |
| SC-013: MetricResult.kind populated for every metric | §10 |