Skip to content

Commit 3bfe126

Browse files
Merge remote-tracking branch 'origin/main' into LeoRoccoBreedt/gh-workflow-test-automation
2 parents 29a03d4 + 8f91cb3 commit 3bfe126

83 files changed

Lines changed: 2356 additions & 12746 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(uv:*)",
5+
"Bash(uv run:*)",
6+
"Bash(git:*)",
7+
"Bash(python:*)",
8+
"Bash(python3:*)",
9+
"Bash(pip:*)",
10+
"Bash(ls:*)",
11+
"Bash(find:*)",
12+
"Bash(grep:*)",
13+
"Bash(jq:*)",
14+
"Bash(echo:*)",
15+
"Read(*)"
16+
],
17+
"deny": [
18+
"Read(.env)",
19+
"Bash(rm -rf /*)",
20+
"Bash(git reset --hard:*)",
21+
"Bash(git rebase:*)",
22+
"Bash(git merge:*)",
23+
"Bash(git branch -D:*)",
24+
"Bash(gh pr merge:*)",
25+
"Bash(gh pr close:*)",
26+
"Bash(gh pr review:*)"
27+
]
28+
}
29+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
name: add-dataset-items
3+
description: Add new items to an Opik evaluation dataset in this repo. Use when the user says "add a case to the dataset", "add items to the Opik dataset", "extend the eval dataset", "seed the dataset", "add test data for the eval", or names new query/expected-output pairs to evaluate. Appends to the example's canonical source data file first, then syncs to Opik via get_or_create_dataset + dataset.insert. Require a target example (or dataset name) and the item content; refuse to invent dataset rows.
4+
---
5+
6+
# Add items to an Opik dataset
7+
8+
An Opik dataset is the set of inputs an example is evaluated against. In this repo each
9+
example keeps a **canonical source file** (e.g. `data/eval_cases.json`) that a loader reads
10+
and inserts into Opik — so dataset items live in two places and must stay in sync.
11+
12+
Reference implementation: [`use-cases/f1_radio_rag`](../../../use-cases/f1_radio_rag)
13+
source `data/eval_cases.json`, loaded by `data.py`, inserted by `build_dataset` in
14+
[`evaluation.py`](../../../use-cases/f1_radio_rag/src/f1_radio_rag/evaluation.py).
15+
16+
## When to refuse
17+
18+
You need both:
19+
20+
- A **target example** (a folder under `use-cases/`, `guides/`, etc.) or an explicit dataset name.
21+
- The **item content** — at minimum the query/input; ideally the expected output and any
22+
context fields the metrics need.
23+
24+
If either is missing, ask one short question. Do not fabricate dataset rows — bad data
25+
silently corrupts every downstream eval and optimization run.
26+
27+
## Step 1 — Match the existing item schema
28+
29+
Read the example's source data file and copy the **exact key set** of existing items. Opik
30+
dataset items are free-form dicts, but the evaluation task and metrics read specific keys.
31+
For f1_radio_rag each item is:
32+
33+
```json
34+
{
35+
"query": "What tyre problems did drivers report?",
36+
"expected_output": "Verstappen reported front-left graining ...",
37+
"messages": ["VER: Front-left is graining ...", "..."],
38+
"assertions": ["Reports the front-left graining ...", "..."]
39+
}
40+
```
41+
42+
New items must provide every key the task/metrics consume. Mismatched or missing keys throw
43+
at eval time (`KeyError` in the task) — not at insert time.
44+
45+
## Step 2 — Append to the canonical source file
46+
47+
Add the new item(s) to the source JSON (the SSOT), preserving formatting. This is the change
48+
that gets committed and reviewed. Keep entries grounded and self-consistent (the expected
49+
output must actually follow from the provided context).
50+
51+
## Step 3 — Sync to Opik
52+
53+
Insert is **idempotent**: Opik content-hashes each item, so re-inserting the whole file only
54+
adds genuinely new rows. The simplest sync is re-running the example's dataset builder.
55+
56+
```python
57+
import opik
58+
59+
client = opik.Opik() # env-driven; never hardcode workspace/keys
60+
dataset = client.get_or_create_dataset(name=DATASET_NAME)
61+
dataset.insert(items) # items = the full list from the source file; identical rows are deduped
62+
```
63+
64+
For f1_radio_rag, `build_dataset(client, load_eval_cases())` does exactly this, and
65+
`uv run f1rag eval` calls it as part of the eval — so adding to `data/eval_cases.json` and
66+
running eval is enough; no separate insert step needed.
67+
68+
## Step 4 — Dry-run / credentials
69+
70+
`dataset.insert` needs `OPIK_API_KEY` + `OPIK_WORKSPACE`. Without them, the example runs in
71+
`DRY_RUN` and prints what it would create instead of calling Opik (see the repo convention in
72+
[AGENTS.md](../../../AGENTS.md)). Verify the new items appear in the dry-run output:
73+
74+
```bash
75+
cd use-cases/f1_radio_rag && uv run f1rag eval # [DRY RUN] lists the dataset items it would create
76+
```
77+
78+
## Verify
79+
80+
- The new item(s) show up in the dry-run listing (or in the Opik UI dataset when creds are set).
81+
- The item's keys match what the eval task and metrics read — otherwise eval throws later.
82+
- The source file is still valid JSON (`jq . data/eval_cases.json`).
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
name: add-eval-suite-items
3+
description: Add new items (plain-English assertions) to an Opik test suite in this repo. Use when the user says "add an assertion", "add a test case to the suite", "extend the eval suite", "add a check to the test suite", "the model should also be tested for X", or describes a pass/fail criterion for an example's output. Appends to the canonical source file, then syncs via get_or_create_test_suite + suite.insert. Require a target example/suite and the assertion text; refuse to invent checks.
4+
---
5+
6+
# Add items to an Opik test suite
7+
8+
A test suite checks an example's output against **plain-English assertions** scored by an
9+
LLM judge — complementary to numeric metrics. Each suite item pairs the input `data` with a
10+
list of `assertions`. Suite-wide assertions and the run policy are set once on the suite.
11+
12+
Reference implementation: `build_suite` in
13+
[`evaluation.py`](../../../use-cases/f1_radio_rag/src/f1_radio_rag/evaluation.py); source
14+
assertions live per-case in
15+
[`data/eval_cases.json`](../../../use-cases/f1_radio_rag/data/eval_cases.json).
16+
17+
## When to refuse
18+
19+
You need both a **target example/suite name** and the **assertion text** (or a clear
20+
criterion you can phrase as one). Don't invent checks — a vague or wrong assertion makes the
21+
judge fail good outputs (or pass bad ones).
22+
23+
## Step 1 — Write good assertions
24+
25+
- **Plain English, judged true/false** about the output. e.g. *"Reports the front-left
26+
graining described in the radio messages."*
27+
- **Grounded in the item's context** — only assert what the provided input/context supports.
28+
- **One claim per assertion.** Split compound checks so a failure points at one thing.
29+
- **Per-item vs global:** item-specific facts → the item's `assertions`. Checks that must
30+
hold for *every* item (e.g. "grounded in the provided messages", "invents nothing") →
31+
`global_assertions` on the suite.
32+
33+
## Step 2 — Append to the canonical source file
34+
35+
Add the assertion(s) to the item's `assertions` array in the source JSON (the SSOT that gets
36+
committed). Keep the existing shape:
37+
38+
```json
39+
{ "data": { "query": "..." }, "assertions": ["Check one ...", "Check two ..."] }
40+
```
41+
42+
## Step 3 — Sync to Opik
43+
44+
```python
45+
suite = client.get_or_create_test_suite(
46+
name=SUITE_NAME,
47+
global_assertions=[
48+
"The answer is grounded in the provided context",
49+
"The answer does not invent events absent from the context",
50+
],
51+
global_execution_policy={"runs_per_item": 2, "pass_threshold": 2},
52+
)
53+
suite.insert([{"data": {"query": c["query"]}, "assertions": c["assertions"]} for c in cases])
54+
```
55+
56+
- `global_execution_policy`: `runs_per_item` re-runs each item to catch nondeterminism;
57+
`pass_threshold` is how many of those runs must pass for the item to pass.
58+
- `suite.insert` is idempotent the same way datasets are — re-running the builder is safe.
59+
60+
For f1_radio_rag, `uv run f1rag eval` rebuilds and runs the suite, so editing the source JSON
61+
and running eval is the whole flow.
62+
63+
## Step 4 — Dry-run / credentials
64+
65+
Needs `OPIK_API_KEY` + `OPIK_WORKSPACE`; otherwise `DRY_RUN` prints the assertions it would
66+
create:
67+
68+
```bash
69+
cd use-cases/f1_radio_rag && uv run f1rag eval # [DRY RUN] lists each query + its assertions
70+
```
71+
72+
## Verify
73+
74+
- New assertions appear in the dry-run listing (or the suite in the Opik UI).
75+
- Each assertion is grounded in that item's context and phrased as a single true/false claim.
76+
- Source file is still valid JSON (`jq . data/eval_cases.json`).

.claude/skills/run-evals/SKILL.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
name: run-evals
3+
description: Run an Opik evaluation for an example in this repo — metrics (evaluate) and/or assertions (run_tests) against a dataset/test suite. Use when the user says "run the eval", "evaluate the example", "run the test suite", "score the model", "check the metrics", "what's the pass rate", or wants to measure an example's quality in Opik. Covers the credentials/DRY_RUN gate, metric selection, experiment naming, and reading results. Require a target example; refuse to guess which one.
4+
---
5+
6+
# Run an Opik evaluation
7+
8+
Two complementary scorers, usually run together:
9+
10+
- **`run_tests(test_suite, ...)`** — judges the plain-English assertions in a test suite.
11+
Returns `.pass_rate` and `.experiment_url`.
12+
- **`evaluate(dataset, ...)`** — runs numeric `scoring_metrics` over a dataset.
13+
14+
Reference: `run_eval` in
15+
[`evaluation.py`](../../../use-cases/f1_radio_rag/src/f1_radio_rag/evaluation.py); CLI
16+
[`cli.py`](../../../use-cases/f1_radio_rag/src/f1_radio_rag/cli.py) `eval` command.
17+
18+
## When to refuse
19+
20+
Need a **target example** (folder + its dataset/suite). If the repo has several, ask which.
21+
Don't run evals blindly — they call LLM-judge models and cost tokens/time.
22+
23+
## Step 1 — Credentials gate
24+
25+
`evaluate` / `run_tests` need `OPIK_API_KEY` + `OPIK_WORKSPACE`, and the judge/generation
26+
models need their provider key (e.g. `ANTHROPIC_API_KEY`). Without Opik creds the example
27+
runs in `DRY_RUN` and prints what it would create. Run the dry-run first to confirm the
28+
dataset/suite content, then set creds for the real run.
29+
30+
```bash
31+
cd use-cases/f1_radio_rag
32+
uv run f1rag eval # DRY_RUN: lists dataset items + assertions
33+
export ANTHROPIC_API_KEY=... OPIK_API_KEY=... OPIK_WORKSPACE=...
34+
uv run f1rag eval # real run: prints pass rate + experiment URL
35+
```
36+
37+
## Step 2 — The task and the shape it returns
38+
39+
The evaluation task is `(item: dict) -> dict`. It runs the live app on the item and returns
40+
the fields the metrics consume — for RAG: `{"input", "output", "context"}`. Reuse the
41+
example's existing task (don't reimplement the app inside the eval).
42+
43+
```python
44+
from opik.evaluation import evaluate, run_tests
45+
from opik.evaluation.metrics import ContextRecall, Hallucination
46+
47+
def task(item: dict) -> dict:
48+
return app_under_test(item["query"]) # -> {"input","output","context"}
49+
```
50+
51+
## Step 3 — Pick metrics that match the failure modes
52+
53+
- `ContextRecall` — did retrieval surface the needed context? (RAG retrieval quality)
54+
- `Hallucination` — is the answer faithful to the context? (faithfulness)
55+
- `AnswerRelevance` — does the answer address the question?
56+
- Add others from `opik.evaluation.metrics` as the use case needs. Each LLM-judge metric
57+
takes `model=...`.
58+
59+
```python
60+
run_tests(test_suite=suite, task=task, model=JUDGE_MODEL, experiment_name=experiment_name())
61+
evaluate(dataset=dataset, task=task,
62+
scoring_metrics=[ContextRecall(model=JUDGE_MODEL), Hallucination(model=JUDGE_MODEL)],
63+
experiment_name=experiment_name())
64+
```
65+
66+
Use a unique `experiment_name` per run (the example does `f"{project}-{secrets.token_hex(3)}"`)
67+
so runs don't overwrite each other in the UI.
68+
69+
## Step 4 — Read results
70+
71+
- `suite_result.pass_rate` — fraction of suite items passing their assertions.
72+
- `suite_result.experiment_url` / `result.get(...)` — link to the run in Opik for per-item
73+
drill-down. Surface the URL to the user.
74+
75+
## Verify
76+
77+
- Dry-run lists the expected dataset/suite content before the real run.
78+
- Real run prints a pass rate and an experiment URL; open it to confirm per-item scores.
79+
- If the task throws `KeyError`, a dataset item is missing a key the task/metrics read — fix
80+
the data (see the `add-dataset-items` skill), not the task.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
name: run-optimizations
3+
description: Run an Opik prompt optimization (opik-optimizer) for an example in this repo, and optionally promote the result to the Prompt Library. Use when the user says "optimize the prompt", "run the optimizer", "improve the prompt", "run Optimization Studio", "tune the prompt", or wants a measured better prompt for an example. Covers the metric-callable shape, reading the score delta + run link, and versioning the winner. Require a target example with an eval dataset; refuse to optimize without a dataset to score against.
4+
---
5+
6+
# Run a prompt optimization
7+
8+
`opik-optimizer` searches for a better prompt by scoring candidates against a dataset with a
9+
metric. It tunes the **prompt and model params** — not the retriever. Retrieval quality is a
10+
separate concern, watched via `ContextRecall` in `run-evals`.
11+
12+
Reference: `run_optimization` in
13+
[`optimization.py`](../../../use-cases/f1_radio_rag/src/f1_radio_rag/optimization.py);
14+
`promote` in [`prompts.py`](../../../use-cases/f1_radio_rag/src/f1_radio_rag/prompts.py);
15+
CLI [`cli.py`](../../../use-cases/f1_radio_rag/src/f1_radio_rag/cli.py) `optimize`/`promote`.
16+
17+
## When to refuse
18+
19+
Need a **target example with an eval dataset** to score against — optimization is meaningless
20+
without a metric over real data. If there's no dataset, route to `add-dataset-items` first.
21+
22+
## Step 1 — Credentials gate
23+
24+
Needs `OPIK_API_KEY` + `OPIK_WORKSPACE` and the model provider key (e.g. `ANTHROPIC_API_KEY`).
25+
Dry-run first:
26+
27+
```bash
28+
cd use-cases/f1_radio_rag
29+
uv run f1rag optimize # DRY_RUN: prints what it would optimize
30+
export ANTHROPIC_API_KEY=... OPIK_API_KEY=... OPIK_WORKSPACE=...
31+
uv run f1rag optimize # real run: prints initial -> optimised score + run link
32+
```
33+
34+
## Step 2 — The metric is a plain callable
35+
36+
Optimizer metrics are **not** the metric classes directly — they're callables
37+
`(dataset_item: dict, llm_output: str) -> float` with `__name__` set (the name shows in the
38+
Opik UI). Wrap an LLM-judge metric to score against something real:
39+
40+
```python
41+
from opik.evaluation.metrics import AnswerRelevance
42+
43+
def _relevance_metric(dataset_item: dict, llm_output: str) -> float:
44+
result = AnswerRelevance(model=JUDGE_MODEL).score(
45+
input=dataset_item["query"], output=llm_output, context=dataset_item["messages"],
46+
)
47+
return result.value
48+
49+
_relevance_metric.__name__ = "answer_relevance"
50+
```
51+
52+
## Step 3 — Build the prompt and run the optimizer
53+
54+
```python
55+
from opik_optimizer import ChatPrompt, MetaPromptOptimizer
56+
57+
prompt = ChatPrompt(name=PROMPT_NAME, system=SYSTEM_PROMPT, user=USER_TEMPLATE, model=GEN_MODEL)
58+
optimizer = MetaPromptOptimizer(model=OPTIMIZER_MODEL, n_threads=4, skip_perfect_score=False)
59+
result = optimizer.optimize_prompt(
60+
prompt=prompt, dataset=dataset, metric=_relevance_metric, max_trials=12, n_samples=8,
61+
)
62+
```
63+
64+
- `ChatPrompt.user` uses single-brace `{field}` placeholders that bind to dataset item fields.
65+
- `skip_perfect_score=False` forces every round even when the baseline already scores high.
66+
- `max_trials` / `n_samples` trade cost for thoroughness — keep them modest for demos.
67+
68+
## Step 4 — Read the result
69+
70+
- `result.initial_score``result.score` — the improvement (or lack of it). Report both.
71+
- `result.get_run_link()` — link to the run in Optimization Studio. Surface it.
72+
- `result.prompt.get_messages()` — the optimised messages, for promotion.
73+
74+
## Step 5 — Promote (optional, only when asked or score improved)
75+
76+
Saving versions the prompt in the Prompt Library; re-using the same `name` appends a version
77+
(upsert — no separate update call, no in-place edit).
78+
79+
```python
80+
client.create_chat_prompt(
81+
name=PROMPT_NAME,
82+
messages=result.prompt.get_messages(),
83+
change_description=f"optimised score {result.score:.3f}",
84+
tags=["optimised"],
85+
)
86+
```
87+
88+
Don't promote a regression. If `result.score <= result.initial_score`, report it and stop —
89+
ask before saving a worse prompt over a working one.
90+
91+
## Verify
92+
93+
- Dry-run prints the prompt/dataset it would optimize before the real run.
94+
- Real run prints `initial -> optimised` score and a run link; open it to inspect trials.
95+
- After promote, the new version appears under the prompt name in the Opik Prompt Library UI.

0 commit comments

Comments
 (0)