Skip to content

Commit 8b1af18

Browse files
docs: add concepts, guides, configuration, and troubleshooting pages (#48)
1 parent 9b21009 commit 8b1af18

12 files changed

Lines changed: 610 additions & 8 deletions

docs/api/evaluators.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
# evalwire.evaluators
22

3-
Built-in evaluator factories. Each factory returns a callable with the standard
4-
evalwire evaluator signature:
3+
Built-in evaluator factories. Each factory returns a callable with the standard evalwire evaluator signature:
54

65
```python
7-
def evaluator(output: Any, expected: dict) -> float | bool: ...
6+
def evaluator(output, expected: dict) -> float | bool: ...
87
```
98

10-
The `expected` dict always contains at minimum an `"expected_output"` key whose
11-
value is parsed by the shared `_parse_expected` helper (handles plain strings,
12-
Python-literal strings such as `"['a','b']"`, and lists).
9+
The `expected` dict always contains at minimum an `"expected_output"` key whose value is parsed by the shared `_parse_expected` helper (handles plain strings, Python-literal strings such as `"['a','b']"`, and pipe-delimited strings).
1310

1411
All factories are importable directly from `evalwire.evaluators`:
1512

@@ -27,6 +24,8 @@ from evalwire.evaluators import (
2724
)
2825
```
2926

27+
For a guide on writing your own evaluators, see [Writing Custom Evaluators](../guides/custom-evaluators.md).
28+
3029
---
3130

3231
## Retrieval

docs/api/runner.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,90 @@
11
# evalwire.runner
22

3+
`ExperimentRunner` auto-discovers experiment subdirectories, fetches the matching Phoenix dataset for each one, and runs the task against every example. Results are stored in Phoenix and returned as a list.
4+
5+
## Basic usage
6+
7+
```python
8+
from phoenix.client import Client
9+
from evalwire.runner import ExperimentRunner
10+
11+
client = Client()
12+
13+
runner = ExperimentRunner(
14+
experiments_dir="experiments",
15+
phoenix_client=client,
16+
concurrency=2,
17+
)
18+
results = runner.run()
19+
```
20+
21+
## Directory layout
22+
23+
Each subdirectory of `experiments_dir` that contains a `task.py` is treated as one experiment. The directory name must exactly match a Phoenix dataset name:
24+
25+
```
26+
experiments/
27+
es_search/ <- matches dataset named "es_search"
28+
task.py <- must define async def task(example)
29+
top_k.py <- evaluator: must define top_k = ...
30+
exact_match.py <- evaluator: must define exact_match = ...
31+
source_router/ <- matches dataset named "source_router"
32+
task.py
33+
is_in.py
34+
```
35+
36+
Subdirectories without `task.py` are silently skipped. Files (non-directories) at the top level of `experiments_dir` are also skipped.
37+
38+
## Experiment naming
39+
40+
Each run in Phoenix is named `{prefix}_{dataset_name}_{iso_timestamp}`. The default prefix is `"eval"`. Override it with `experiment_name_prefix`:
41+
42+
```python
43+
results = runner.run(experiment_name_prefix="nightly")
44+
# produces e.g. "nightly_es_search_2025-01-15T09:30:00"
45+
```
46+
47+
## Running a subset
48+
49+
Pass `names` to run only specific experiments:
50+
51+
```python
52+
results = runner.run(names=["es_search"])
53+
```
54+
55+
## Dry run
56+
57+
Set `dry_run=True` (or `dry_run=N` to limit to N examples) to execute tasks without uploading results to Phoenix. Useful for smoke-testing your task code:
58+
59+
```python
60+
runner = ExperimentRunner(
61+
experiments_dir="experiments",
62+
phoenix_client=client,
63+
dry_run=3,
64+
)
65+
runner.run()
66+
```
67+
68+
## Async tasks
69+
70+
Tasks are `async` functions. evalwire wraps them in a per-thread event loop so Phoenix's synchronous runner can call them. The loop is kept open between calls (unlike `asyncio.run()`) so that async I/O libraries which reuse connections across calls work correctly.
71+
72+
## Error behaviour
73+
74+
If any experiment fails (dataset not found, task raises, evaluator raises), `runner.run()` raises `SystemExit(1)` after all experiments complete so that CI pipelines fail loudly. Successful experiments are still returned.
75+
76+
## Pitfalls
77+
78+
- The dataset name must match the directory name exactly (case-sensitive).
79+
- At least one evaluator file is required per experiment. Phoenix raises an error if `run_experiment` is called with an empty evaluator list.
80+
- Relative imports inside experiment modules work because evalwire adds the parent of `experiments_dir` to `sys.path` during discovery. It is removed again afterwards.
81+
82+
## See also
83+
84+
- [Concepts](../concepts.md) for the experiment lifecycle
85+
- [Configuration Reference](../configuration.md) for `evalwire.toml` keys
86+
- [CLI Reference](cli.md) for `evalwire run`
87+
88+
---
89+
390
::: evalwire.runner

docs/api/uploader.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,54 @@
11
# evalwire.uploader
22

3+
`DatasetUploader` reads a CSV testset and uploads it to Arize Phoenix as one named dataset per unique tag value. It handles three conflict modes (`skip`, `overwrite`, `append`) and supports multi-tag rows via a configurable delimiter.
4+
5+
## Basic usage
6+
7+
```python
8+
from phoenix.client import Client
9+
from evalwire.uploader import DatasetUploader
10+
11+
client = Client()
12+
13+
uploader = DatasetUploader(
14+
csv_path="data/testset.csv",
15+
phoenix_client=client,
16+
)
17+
datasets = uploader.upload(on_exist="skip")
18+
print(datasets) # {"es_search": <Dataset>, "source_router": <Dataset>}
19+
```
20+
21+
## CSV format
22+
23+
The CSV must contain at least a tag column, one input column, and one expected-output column:
24+
25+
```csv
26+
user_query,expected_output,tags
27+
"find cycling paths","url-a | url-b","es_search | source_router"
28+
"find parks","url-c","es_search"
29+
```
30+
31+
Pipe-delimited values in any column are split into lists. A row with `tags = "es_search | source_router"` is added to both datasets.
32+
33+
## Conflict modes
34+
35+
| `on_exist` | Behaviour |
36+
|---|---|
37+
| `"skip"` | Do nothing if the dataset already exists. |
38+
| `"overwrite"` | Delete the existing dataset and re-create it. |
39+
| `"append"` | Call `add_examples_to_dataset` on the existing dataset. If not found, create it. |
40+
41+
## Pitfalls
42+
43+
- Phoenix raises `ValueError` (not a Phoenix-specific exception) when `get_dataset` is called for a non-existent dataset. evalwire catches this and creates the dataset instead.
44+
- There is no official delete method in the Phoenix Python client. evalwire calls the REST endpoint `DELETE /v1/datasets/{id}` directly for the `overwrite` mode.
45+
- Creating a dataset with a name that already exists returns a 409 Conflict error, not a new version. Use `on_exist="overwrite"` to replace it.
46+
47+
## See also
48+
49+
- [Configuration Reference](../configuration.md) for `evalwire.toml` keys
50+
- [CLI Reference](cli.md) for `evalwire upload`
51+
52+
---
53+
354
::: evalwire.uploader

docs/changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{%
2+
include-markdown "../CHANGELOG.md"
3+
%}

docs/concepts.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Concepts
2+
3+
This page explains the mental model behind evalwire before you dive into the quick start or API reference.
4+
5+
## The four building blocks
6+
7+
### Datasets
8+
9+
A **dataset** is a named collection of test examples stored in Arize Phoenix. Each example has input fields (e.g. `user_query`) and expected-output fields (e.g. `expected_output`). evalwire creates datasets by uploading a CSV testset and splitting it by a tag column.
10+
11+
One CSV row can belong to multiple datasets. Just pipe-delimit the tag value:
12+
13+
```
14+
user_query,expected_output,tags
15+
"find cycling paths","url-a | url-b","es_search | source_router"
16+
```
17+
18+
This row appears in both the `es_search` and `source_router` datasets.
19+
20+
### Experiments
21+
22+
An **experiment** is the result of running a task against a dataset and scoring each output with one or more evaluators. Phoenix records every experiment run with a timestamp so you can compare results across code changes.
23+
24+
evalwire discovers experiments by scanning a directory. Each subdirectory that contains a `task.py` is treated as one experiment. The directory name must match a Phoenix dataset name:
25+
26+
```
27+
experiments/
28+
es_search/ ← matched to the "es_search" Phoenix dataset
29+
task.py
30+
top_k.py ← evaluator
31+
source_router/ ← matched to the "source_router" Phoenix dataset
32+
task.py
33+
is_in.py ← evaluator
34+
```
35+
36+
### Tasks
37+
38+
A **task** is an `async` function that receives a Phoenix example object and returns the output to be scored. Its job is to call your system under test and return a result in whatever form your evaluators expect.
39+
40+
```python
41+
# experiments/es_search/task.py
42+
async def task(example):
43+
user_query = example.input["user_query"]
44+
return await my_retrieval_function(user_query)
45+
```
46+
47+
The function must be named `task`.
48+
49+
### Evaluators
50+
51+
An **evaluator** is a plain callable with the signature:
52+
53+
```python
54+
def evaluator(output, expected: dict) -> float | bool: ...
55+
```
56+
57+
Each evaluator file in an experiment directory is auto-loaded. The callable must share its name with the file:
58+
59+
```python
60+
# experiments/es_search/top_k.py
61+
from evalwire.evaluators import make_top_k_evaluator
62+
63+
top_k = make_top_k_evaluator(K=5)
64+
```
65+
66+
Return `float` (0.0–1.0) for graded scoring or `bool` for pass/fail. Phoenix displays both.
67+
68+
## The experiment lifecycle
69+
70+
```
71+
CSV testset
72+
73+
│ evalwire upload
74+
75+
Phoenix Datasets (one per unique tag)
76+
77+
│ evalwire run
78+
79+
Task function (called once per example)
80+
81+
82+
Output
83+
84+
│ evaluator(output, expected)
85+
86+
Experiment Results (stored in Phoenix, visible in UI)
87+
```
88+
89+
1. **Upload**: `evalwire upload` reads your CSV, groups rows by the tag column, and creates or updates one Phoenix dataset per unique tag value.
90+
2. **Run**: `evalwire run` scans the experiments directory, matches each subdirectory to a Phoenix dataset by name, calls `task` on every example, and scores the output with each evaluator file found in that directory.
91+
3. **Compare**: open the Phoenix UI, navigate to the dataset, and switch to the **Experiments** tab to compare runs side-by-side.
92+
93+
## How `expected` is structured
94+
95+
Inside every evaluator, the `expected` parameter is a dict containing all output columns from the original CSV row. The most important key is `"expected_output"`, which evalwire parses with `_parse_expected`:
96+
97+
- Plain string `"answer"``["answer"]`
98+
- Pipe-delimited string `"a | b"``["a", "b"]`
99+
- Python-literal string `"['a', 'b']"``["a", "b"]`
100+
- Already a list → returned as-is
101+
102+
Most built-in evaluators read `expected["expected_output"]` for you. When writing a custom evaluator you can also access any other column directly: `expected["my_column"]`.

docs/configuration.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Configuration Reference
2+
3+
evalwire can be driven entirely from a `evalwire.toml` file at the root of your project. CLI flags always take precedence over config file values, which take precedence over hardcoded defaults.
4+
5+
## Precedence
6+
7+
```
8+
CLI flag > evalwire.toml > hardcoded default
9+
```
10+
11+
## Example file
12+
13+
```toml
14+
[dataset]
15+
csv_path = "data/testset.csv"
16+
on_exist = "skip"
17+
input_keys = ["user_query"]
18+
output_keys = ["expected_output"]
19+
tag_column = "tags"
20+
delimiter = "|"
21+
22+
[experiments]
23+
dir = "experiments"
24+
prefix = "eval"
25+
concurrency = 4
26+
```
27+
28+
## `[dataset]` section
29+
30+
Used by `evalwire upload`.
31+
32+
| Key | Type | Default | Description |
33+
|---|---|---|---|
34+
| `csv_path` | string | none (required) | Path to the CSV testset file. |
35+
| `on_exist` | `"skip"` / `"overwrite"` / `"append"` | `"skip"` | How to handle a dataset that already exists in Phoenix. |
36+
| `input_keys` | list of strings | `["user_query"]` | CSV column names treated as example inputs. |
37+
| `output_keys` | list of strings | `["expected_output"]` | CSV column names treated as expected outputs. |
38+
| `tag_column` | string | `"tags"` | CSV column used to split rows into separate datasets. |
39+
| `delimiter` | string | `"\|"` | Character used to assign a row to multiple datasets. |
40+
41+
### `on_exist` modes
42+
43+
| Value | Behaviour |
44+
|---|---|
45+
| `skip` | Leave the existing dataset untouched. |
46+
| `overwrite` | Delete the existing dataset and re-create it from the CSV. |
47+
| `append` | Add the new rows to the existing dataset. |
48+
49+
## `[experiments]` section
50+
51+
Used by `evalwire run`.
52+
53+
| Key | Type | Default | Description |
54+
|---|---|---|---|
55+
| `dir` | string | `"experiments"` | Path to the experiments directory. |
56+
| `prefix` | string | `"eval"` | Prefix prepended to every experiment name in Phoenix. |
57+
| `concurrency` | integer | `1` | Number of experiments to run in parallel. |
58+
59+
## `[phoenix]` section
60+
61+
Reserved for future use. Currently read by `get_phoenix_config()` but not consumed by the CLI.
62+
63+
## CLI flag reference
64+
65+
### `evalwire upload`
66+
67+
| Flag | Config key | Default |
68+
|---|---|---|
69+
| `--csv PATH` | `dataset.csv_path` | none |
70+
| `--on-exist MODE` | `dataset.on_exist` | `skip` |
71+
| `--input-keys COLS` | `dataset.input_keys` | `user_query` |
72+
| `--output-keys COLS` | `dataset.output_keys` | `expected_output` |
73+
| `--tag-column COL` | `dataset.tag_column` | `tags` |
74+
| `--delimiter CHAR` | `dataset.delimiter` | `\|` |
75+
| `--config PATH` | n/a | `./evalwire.toml` |
76+
77+
### `evalwire run`
78+
79+
| Flag | Config key | Default |
80+
|---|---|---|
81+
| `--experiments PATH` | `experiments.dir` | `experiments` |
82+
| `--name NAME` | n/a | all experiments |
83+
| `--prefix PREFIX` | `experiments.prefix` | `eval` |
84+
| `--concurrency N` | `experiments.concurrency` | `1` |
85+
| `--dry-run [N]` | n/a | off |
86+
| `--config PATH` | n/a | `./evalwire.toml` |
87+
88+
`--name` is repeatable: `evalwire run --name es_search --name source_router`.
89+
90+
`--dry-run` runs the task but does not upload results to Phoenix. Optionally accepts a number to limit the run to the first N examples per dataset.

0 commit comments

Comments
 (0)