evalbar 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalbar-0.1.0/.env.example +5 -0
- evalbar-0.1.0/.github/workflows/ci.yml +43 -0
- evalbar-0.1.0/.github/workflows/release.yml +48 -0
- evalbar-0.1.0/.gitignore +14 -0
- evalbar-0.1.0/LICENSE +21 -0
- evalbar-0.1.0/PKG-INFO +249 -0
- evalbar-0.1.0/README.md +227 -0
- evalbar-0.1.0/docs/design.md +113 -0
- evalbar-0.1.0/docs/roadmap.md +28 -0
- evalbar-0.1.0/examples/ci/evalbar-gate.yml +56 -0
- evalbar-0.1.0/examples/feedbackos/adapter.py +131 -0
- evalbar-0.1.0/examples/feedbackos/cases.jsonl +50 -0
- evalbar-0.1.0/examples/feedbackos/eval.py +61 -0
- evalbar-0.1.0/examples/feedbackos/prompts/v1.txt +55 -0
- evalbar-0.1.0/examples/feedbackos/prompts/v2.txt +63 -0
- evalbar-0.1.0/examples/quickstart/baseline.json +255 -0
- evalbar-0.1.0/examples/quickstart/cases.yaml +13 -0
- evalbar-0.1.0/examples/quickstart/eval.py +42 -0
- evalbar-0.1.0/examples/rag/cases.yaml +27 -0
- evalbar-0.1.0/examples/rag/eval.py +85 -0
- evalbar-0.1.0/pyproject.toml +48 -0
- evalbar-0.1.0/src/evalbar/__init__.py +34 -0
- evalbar-0.1.0/src/evalbar/cli.py +181 -0
- evalbar-0.1.0/src/evalbar/dataset.py +78 -0
- evalbar-0.1.0/src/evalbar/diff.py +265 -0
- evalbar-0.1.0/src/evalbar/html.py +156 -0
- evalbar-0.1.0/src/evalbar/llm.py +50 -0
- evalbar-0.1.0/src/evalbar/report.py +174 -0
- evalbar-0.1.0/src/evalbar/runner.py +209 -0
- evalbar-0.1.0/src/evalbar/scorers.py +238 -0
- evalbar-0.1.0/src/evalbar/trace.py +163 -0
- evalbar-0.1.0/tests/test_cli.py +136 -0
- evalbar-0.1.0/tests/test_dataset.py +65 -0
- evalbar-0.1.0/tests/test_diff.py +139 -0
- evalbar-0.1.0/tests/test_feedbackos_adapter.py +90 -0
- evalbar-0.1.0/tests/test_html.py +73 -0
- evalbar-0.1.0/tests/test_judge_recall.py +126 -0
- evalbar-0.1.0/tests/test_report.py +67 -0
- evalbar-0.1.0/tests/test_runner.py +120 -0
- evalbar-0.1.0/tests/test_scorers.py +86 -0
- evalbar-0.1.0/tests/test_trace.py +109 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Copy to .env (gitignored). evalbar run loads it; variables already set in the shell win.
|
|
2
|
+
# Needed for examples/feedbackos (Groq hosts FeedbackOS's model, openai/gpt-oss-120b).
|
|
3
|
+
GROQ_API_KEY=
|
|
4
|
+
# Optional: seconds between case starts (Groq free tier: 8K tokens/minute).
|
|
5
|
+
EVALBAR_MIN_INTERVAL=9
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
- uses: actions/setup-python@v7
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
cache: pip
|
|
24
|
+
- run: python -m pip install -e ".[dev]"
|
|
25
|
+
- run: ruff check .
|
|
26
|
+
- run: pytest -q
|
|
27
|
+
|
|
28
|
+
# Dogfood the gate on the offline quickstart (mock LLM responses, no API key):
|
|
29
|
+
# a change that lowers any score below the committed baseline fails this job.
|
|
30
|
+
eval-gate:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v7
|
|
34
|
+
- uses: actions/setup-python@v7
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
cache: pip
|
|
38
|
+
- run: python -m pip install -e .
|
|
39
|
+
- run: evalbar run examples/quickstart/eval.py --out runs/head.json --quiet
|
|
40
|
+
- name: Compare with baseline
|
|
41
|
+
run: |
|
|
42
|
+
evalbar diff examples/quickstart/baseline.json runs/head.json \
|
|
43
|
+
--fail-on-drop 0 --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag (v0.1.0, ...) is pushed. Uses PyPI trusted publishing:
|
|
4
|
+
# no API token is stored in GitHub. One-time setup on pypi.org (account owner only):
|
|
5
|
+
# Account > Publishing > Add a pending publisher
|
|
6
|
+
# project: evalbar, owner: MilindDevX, repository: evalbar, workflow: release.yml, environment: pypi
|
|
7
|
+
# Then create the "pypi" environment under the repo's Settings > Environments.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
tags: ["v*"]
|
|
12
|
+
|
|
13
|
+
permissions: {}
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
- uses: actions/setup-python@v7
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
- run: python -m pip install build twine
|
|
26
|
+
- name: Tag must match the package version
|
|
27
|
+
run: |
|
|
28
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
29
|
+
test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != v$version"; exit 1; }
|
|
30
|
+
- run: python -m build
|
|
31
|
+
- run: twine check --strict dist/*
|
|
32
|
+
- uses: actions/upload-artifact@v7
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
needs: build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment: pypi
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v8
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
evalbar-0.1.0/.gitignore
ADDED
evalbar-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Milind Bansal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
evalbar-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: evalbar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small LLM eval and tracing harness: run any pipeline over a test set, trace tokens/latency/cost per case, score it, and gate CI on quality drops.
|
|
5
|
+
Project-URL: Repository, https://github.com/MilindDevX/evalbar
|
|
6
|
+
Author: Milind Bansal
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: jsonschema>=4.20
|
|
15
|
+
Requires-Dist: litellm!=1.82.7,!=1.82.8,>=1.80
|
|
16
|
+
Requires-Dist: pyyaml>=6
|
|
17
|
+
Requires-Dist: rich>=13
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# evalbar
|
|
24
|
+
|
|
25
|
+
Small eval and tracing harness for LLM pipelines. Point it at any Python function, give it a file of labelled test cases, and get a scored run with a trace per case: LLM calls, prompt and completion tokens, latency and cost. Save the run as JSON so the next change can be compared against it.
|
|
26
|
+
|
|
27
|
+
- **Any callable**: an LLM call, a RAG (retrieval-augmented generation) chain or an agent; evalbar only sees `pipeline(input) -> output`.
|
|
28
|
+
- **Plain datasets**: JSONL (one JSON object per line) or YAML.
|
|
29
|
+
- **Traces**: tokens, latency and list-price cost per case through [LiteLLM](https://github.com/BerriAI/litellm), so any provider works; tool calls via a decorator.
|
|
30
|
+
- **Scorers**: exact match, contains, regex, JSON-schema validity, retrieval recall@k, and an LLM-as-judge with a rubric whose own cost is reported apart from the pipeline's.
|
|
31
|
+
- **Diffs and CI gates**: `evalbar diff base.json head.json --fail-on-drop 0.02` names the regressed cases and exits 1 when quality drops (or cost rises past `--max-cost-increase`).
|
|
32
|
+
- **Reports**: a terminal table, a JSON run file, and a single self-contained HTML page (`evalbar html`).
|
|
33
|
+
- **Honest numbers**: a crashed case fails every scorer it applies to instead of disappearing; an unknown price is reported as unknown, never as $0.
|
|
34
|
+
|
|
35
|
+
Status: M1–M3 of the [roadmap](docs/roadmap.md) are done; version 0.1.0 is ready to release but not yet on PyPI. Design and comparison with promptfoo, DeepEval and Ragas: [docs/design.md](docs/design.md).
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/MilindDevX/evalbar && cd evalbar
|
|
41
|
+
uv venv --python 3.12 && uv pip install -e ".[dev]"
|
|
42
|
+
.venv/bin/evalbar run examples/quickstart/eval.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
After the 0.1.0 release: `pip install evalbar`.
|
|
46
|
+
|
|
47
|
+
The quickstart needs no API key (LiteLLM's `mock_response` stands in for the model) and prints a score table, a usage table and the failing case with its reason. The run is saved under `runs/`; re-print it with `evalbar show runs/<file>.json`.
|
|
48
|
+
|
|
49
|
+
## Write an eval
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# my_eval.py
|
|
53
|
+
import json
|
|
54
|
+
|
|
55
|
+
from evalbar import Eval, completion, load_dataset, traced_tool
|
|
56
|
+
from evalbar.scorers import exact, json_schema, regex
|
|
57
|
+
|
|
58
|
+
@traced_tool # recorded in the case trace: args, result, latency, errors
|
|
59
|
+
def search(query: str) -> list[str]:
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def pipeline(case_input: dict) -> dict:
|
|
63
|
+
docs = search(case_input["question"])
|
|
64
|
+
reply = completion( # litellm.completion + tokens/latency/cost in the trace
|
|
65
|
+
model="groq/openai/gpt-oss-120b",
|
|
66
|
+
messages=[{"role": "user", "content": f"{docs}\n\n{case_input['question']}"}],
|
|
67
|
+
response_format={"type": "json_object"},
|
|
68
|
+
)
|
|
69
|
+
return json.loads(reply.choices[0].message.content)
|
|
70
|
+
|
|
71
|
+
my_eval = Eval(
|
|
72
|
+
name="support-bot",
|
|
73
|
+
pipeline=pipeline,
|
|
74
|
+
dataset=load_dataset("cases.jsonl"),
|
|
75
|
+
scorers=[
|
|
76
|
+
exact("label"), # output["label"] == expected["label"]
|
|
77
|
+
json_schema(SCHEMA), # output validates
|
|
78
|
+
regex("answer", r"^The user", must_match=False), # style rule
|
|
79
|
+
],
|
|
80
|
+
min_interval_s=2.0, # simple rate limit
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
evalbar run my_eval.py --out runs/baseline.json # add :name when the file defines several Evals
|
|
86
|
+
evalbar run my_eval.py --limit 3 # smoke test before spending quota
|
|
87
|
+
evalbar show runs/baseline.json
|
|
88
|
+
evalbar html runs/baseline.json # writes runs/baseline.html
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A case in `cases.jsonl`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{"id": "refund-01", "input": {"question": "How do I get a refund?"}, "expected": {"label": "billing"}, "metadata": {"split": "dev"}}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A scorer is any `(output, case) -> Score | None` function. Built-ins return `None` when the case has no expectation for them (for example `contains("secondary_themes")` on a case without `expected.secondary_themes`), so every scorer is averaged only over the cases it applies to.
|
|
98
|
+
|
|
99
|
+
`evalbar run` loads `.env` from the current folder if present (variables already set in the shell win); see `.env.example`.
|
|
100
|
+
|
|
101
|
+
## Retrieval and LLM-as-judge
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from evalbar.scorers import llm_judge, recall_at_k
|
|
105
|
+
|
|
106
|
+
recall_at_k(k=3) # share of expected["relevant"] ids in output["retrieved"][:3]
|
|
107
|
+
llm_judge(
|
|
108
|
+
"The answer is correct, supported by the context, and invents nothing.",
|
|
109
|
+
model="groq/openai/gpt-oss-120b",
|
|
110
|
+
pass_threshold=0.7,
|
|
111
|
+
expects="judge_me", # optional: only cases that carry this expected key
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The judge sees the rubric, the case input, the expected values and the output, and must reply `{"score": 0..1, "reason": "..."}`. Its calls are traced with role `judge`, so the report shows judge cost separately and `cost` stays the cost of the system under test. A judge that errors or replies with something unparseable fails the case with a `judge error` reason instead of being skipped.
|
|
116
|
+
|
|
117
|
+
[`examples/rag/`](examples/rag) is a tiny RAG pipeline: a keyword retriever over 8 documents of a fictional notes app, and an answer model. It runs offline by default; `EVALBAR_LIVE=1` uses Groq for answers and for the judge. Measured 2026-10-23, 8 questions:
|
|
118
|
+
|
|
119
|
+
| scorer | pass rate | what it caught |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| recall@2 | 7/8 | `money-back` ("Can I get my money back?") shares no words with the refund document |
|
|
122
|
+
| contains (expected fact in answer) | 4/8 | the same miss, plus 3 correct but terse answers ("Yes, the Business plan supports SSO with Okta." lacks "SAML") |
|
|
123
|
+
| judge (answered, faithful, threshold 0.7) | 7/8 | only `money-back`, where the model answered "I don't know" |
|
|
124
|
+
|
|
125
|
+
The keyword check penalised correct answers; the judge and recall@k agreed on the one real failure. The judge cost $0.00085, about 2× the pipeline's own $0.00039 for these 8 cases. The judge here is the same model as the pipeline, which can bias it toward its own answers; set `EVALBAR_JUDGE_MODEL` to use a different one.
|
|
126
|
+
|
|
127
|
+
## Compare runs and gate CI
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
evalbar diff runs/baseline.json runs/head.json # report only, exit 0
|
|
131
|
+
evalbar diff runs/baseline.json runs/head.json --fail-on-drop 0.02 # exit 1 if any pass rate drops > 2 points
|
|
132
|
+
evalbar diff runs/baseline.json runs/head.json --fail-on-drop 0 --scorer exact:label --max-cost-increase 0.2
|
|
133
|
+
evalbar diff runs/baseline.json runs/head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The diff compares only the cases both runs share, so adding or removing cases cannot fake a gain. For each scorer it shows the pass rate before and after and the ids of cases that regressed or got fixed (a case that newly crashes counts as a regression). It also shows cost, tokens and LLM latency per case.
|
|
137
|
+
|
|
138
|
+
Reversing the FeedbackOS comparison below (production prompt as the baseline, old prompt as the change) is a realistic regression, and the gate catches it:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
$ evalbar diff runs/feedbackos-v2.json runs/feedbackos-v1.json --fail-on-drop 0.02
|
|
142
|
+
FAIL: exact:sentiment dropped 6.0 pts (100.0% -> 94.0%), allowed 2.0; regressed: mixed-01, mixed-02, mixed-03
|
|
143
|
+
FAIL: theme+sentiment dropped 4.0 pts (96.0% -> 92.0%), allowed 2.0; regressed: mixed-01, mixed-02
|
|
144
|
+
FAIL: contains:secondary_themes dropped 100.0 pts (100.0% -> 0.0%), allowed 2.0; regressed: mixed-01, ..., mixed-08
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Forward (v1 to v2), the quality gate passes, but `--max-cost-increase 0.10` fails because v2 costs 13% more per item. The job surfaces the trade-off instead of hiding it.
|
|
148
|
+
|
|
149
|
+
GitHub Actions:
|
|
150
|
+
|
|
151
|
+
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs this repo's tests on Python 3.10–3.13. It also dogfoods the gate: it runs the offline quickstart and diffs it against the committed [`examples/quickstart/baseline.json`](examples/quickstart/baseline.json), so no API key is needed.
|
|
152
|
+
- [`examples/ci/evalbar-gate.yml`](examples/ci/evalbar-gate.yml) is a template to copy into your project. It runs your eval against a real model on pull requests, compares it with a committed baseline, writes the table to the job summary and uploads the run file.
|
|
153
|
+
|
|
154
|
+
## First real user: FeedbackOS's feedback classifier
|
|
155
|
+
|
|
156
|
+
FeedbackOS (Milind's feedback-triage SaaS) classifies customer feedback into a theme (`bug_problem`, `feature_request`, `usability_issue`, `question_support`, `praise`, `other`), a sentiment and a product area with `openai/gpt-oss-120b` on Groq. On 2026-10-12 its prompt changed: v2 added `secondary_themes` (praise inside a complaint is kept) and a stricter "mixed" sentiment rule.
|
|
157
|
+
|
|
158
|
+
[`examples/feedbackos/`](examples/feedbackos) reruns both prompt versions with the production settings:
|
|
159
|
+
|
|
160
|
+
- `prompts/v1.txt`, `prompts/v2.txt`: system prompts copied verbatim from FeedbackOS's git history (v1 = `9a896d9`, v2 = `59e4902`, the production prompt).
|
|
161
|
+
- `adapter.py`: a Python port of `classifyFeedback` (same user prompt, model, `temperature=0`, 512 max tokens, JSON mode, `reasoning_effort="low"`, short-text guard, product-area fallback).
|
|
162
|
+
- `cases.jsonl`: 50 synthetic, hand-labelled cases (8 bug, 8 feature request, 7 usability, 7 question, 7 praise, 5 other, 8 mixed praise+problem). No real user data.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cp .env.example .env # then put your GROQ_API_KEY in .env with an editor
|
|
166
|
+
evalbar run examples/feedbackos/eval.py:v1 --out runs/feedbackos-v1.json
|
|
167
|
+
evalbar run examples/feedbackos/eval.py:v2 --out runs/feedbackos-v2.json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Results
|
|
171
|
+
|
|
172
|
+
Measured 2026-10-22 on Groq, one run per prompt, 50 cases each, 49 LLM calls each (one case is under 5 words and skips the model, as in production). evalbar 0.1.0.dev0, LiteLLM 1.106.2. Raw run files are not committed; rerun both evals, then `evalbar diff runs/feedbackos-v1.json runs/feedbackos-v2.json --format markdown` regenerates the comparison.
|
|
173
|
+
|
|
174
|
+
| metric | v1 (single theme) | v2 (production) | change |
|
|
175
|
+
|---|---|---|---|
|
|
176
|
+
| theme accuracy (n=50) | 48/50 (96%) | 48/50 (96%) | 0 |
|
|
177
|
+
| sentiment accuracy (n=50) | 47/50 (94%) | **50/50 (100%)** | +6 pts |
|
|
178
|
+
| theme and sentiment both right (n=50) | 46/50 (92%) | **48/50 (96%)** | +4 pts |
|
|
179
|
+
| product-area accuracy (n=50) | 48/50 (96%) | 48/50 (96%) | 0 |
|
|
180
|
+
| praise kept in mixed feedback (n=8) | 0/8 | **8/8** | +8 cases |
|
|
181
|
+
| valid output schema | 50/50 | 50/50 | 0 |
|
|
182
|
+
| summary at most 15 words | 50/50 | 50/50 | 0 |
|
|
183
|
+
| prompt tokens per case | 778 | 929 | +19% |
|
|
184
|
+
| completion tokens per case | 107 | 118 | +10% |
|
|
185
|
+
| cost per 1,000 items (Groq list price) | $0.16 | $0.18 | +13% |
|
|
186
|
+
| LLM latency p50 / p95 | 0.90s / 1.83s | 0.80s / 1.63s | within noise |
|
|
187
|
+
|
|
188
|
+
What changed and what did not:
|
|
189
|
+
|
|
190
|
+
- **All of v2's gain is on mixed feedback** (praise next to a problem or request). On the 42 single-theme cases both prompts score the same. On the 8 mixed cases, v1 never keeps the praise (it has no `secondary_themes` field) and gets the sentiment wrong 3 times: `mixed-01` negative, `mixed-02` positive, `mixed-03` negative, all expected mixed. v2 gets all 8 right.
|
|
191
|
+
- **The price is a longer prompt**: +19% prompt tokens, +13% cost per item. On Groq's free tier the actual spend is $0; the list price for both runs together was $0.017 (96,591 tokens).
|
|
192
|
+
- **Shared misses** (both prompts, so not caused by the change):
|
|
193
|
+
- `other-01`: a SOC 2 / vendor-questionnaire request is classified `question_support`, although the prompt's own boundary rule says administrative requests are `other`. A real prompt-following gap.
|
|
194
|
+
- `other-05`: "ok see attached" (under 5 words) skips the model and gets product area `Dashboard`, because FeedbackOS's short-text guard uses the first product area instead of the `Other` fallback. A real FeedbackOS bug, found by this eval.
|
|
195
|
+
- `feat-03` (webhooks: `API` vs `Integrations`) and `mixed-03` (a broken "Remember me" box: `bug_problem` vs `usability_issue`) are arguably label ambiguity rather than model errors.
|
|
196
|
+
|
|
197
|
+
Caveats: 50 synthetic cases written by the evalbar author and labelled to the v2 contract, so one case is 2 points and these numbers say nothing about production accuracy. `praise kept` measures a field v1 cannot produce, so it shows the contract change, not v1 failing at its own job. Each prompt was run once; `gpt-oss-120b` at temperature 0 is not guaranteed deterministic.
|
|
198
|
+
|
|
199
|
+
What the CLI prints for v2 (trimmed):
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
evalbar run: feedbackos-v2 50 cases, 0 errors, started 2026-10-22T13:10:02+00:00
|
|
203
|
+
Scores
|
|
204
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓
|
|
205
|
+
┃ scorer ┃ cases ┃ passed ┃ pass rate ┃ mean ┃
|
|
206
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━┩
|
|
207
|
+
│ exact:theme │ 50 │ 48 │ 96.0% │ 0.960 │
|
|
208
|
+
│ exact:sentiment │ 50 │ 50 │ 100.0% │ 1.000 │
|
|
209
|
+
│ exact:product_area │ 50 │ 48 │ 96.0% │ 0.960 │
|
|
210
|
+
│ theme+sentiment │ 50 │ 48 │ 96.0% │ 0.960 │
|
|
211
|
+
│ contains:secondary_themes │ 8 │ 8 │ 100.0% │ 1.000 │
|
|
212
|
+
│ valid_output │ 50 │ 50 │ 100.0% │ 1.000 │
|
|
213
|
+
│ summary<=15 words │ 50 │ 50 │ 100.0% │ 1.000 │
|
|
214
|
+
└───────────────────────────┴───────┴────────┴───────────┴───────┘
|
|
215
|
+
Usage
|
|
216
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
|
|
217
|
+
┃ metric ┃ value ┃
|
|
218
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
|
|
219
|
+
│ LLM calls │ 49 │
|
|
220
|
+
│ prompt tokens │ 46,454 │
|
|
221
|
+
│ completion tokens │ 5,887 │
|
|
222
|
+
│ cost (list price) │ $0.009233 │
|
|
223
|
+
│ cost per case │ $0.000185 │
|
|
224
|
+
│ case latency p50 / p95 │ 0.80s / 1.62s │
|
|
225
|
+
│ LLM latency p50 / p95 │ 0.80s / 1.63s │
|
|
226
|
+
└────────────────────────┴───────────────┘
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Releasing (maintainer)
|
|
230
|
+
|
|
231
|
+
`.github/workflows/release.yml` builds, runs `twine check --strict` and publishes to PyPI with trusted publishing when a `v*` tag matching `pyproject.toml`'s version is pushed. One-time setup: add a pending publisher on pypi.org (project `evalbar`, owner `MilindDevX`, repo `evalbar`, workflow `release.yml`, environment `pypi`) and create the `pypi` environment in the repo settings.
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
uv venv --python 3.12 && uv pip install -e ".[dev]"
|
|
237
|
+
.venv/bin/python -m pytest -q
|
|
238
|
+
.venv/bin/ruff check .
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Costs come from the price LiteLLM computes during the call (`response_cost`), because `completion_cost()` on a Groq response looks the model up under OpenAI and fails; see [design decision 4](docs/design.md#design-decisions).
|
|
242
|
+
|
|
243
|
+
Tests run offline: LiteLLM's `mock_response` exercises the real `completion()` path, and LiteLLM's bundled price table is used (`LITELLM_LOCAL_MODEL_COST_MAP=True`) so costs are reproducible and tied to the LiteLLM version recorded in each run file.
|
|
244
|
+
|
|
245
|
+
LiteLLM 1.82.7 and 1.82.8 were compromised releases on PyPI (March 2026); the dependency range excludes them.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|
evalbar-0.1.0/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# evalbar
|
|
2
|
+
|
|
3
|
+
Small eval and tracing harness for LLM pipelines. Point it at any Python function, give it a file of labelled test cases, and get a scored run with a trace per case: LLM calls, prompt and completion tokens, latency and cost. Save the run as JSON so the next change can be compared against it.
|
|
4
|
+
|
|
5
|
+
- **Any callable**: an LLM call, a RAG (retrieval-augmented generation) chain or an agent; evalbar only sees `pipeline(input) -> output`.
|
|
6
|
+
- **Plain datasets**: JSONL (one JSON object per line) or YAML.
|
|
7
|
+
- **Traces**: tokens, latency and list-price cost per case through [LiteLLM](https://github.com/BerriAI/litellm), so any provider works; tool calls via a decorator.
|
|
8
|
+
- **Scorers**: exact match, contains, regex, JSON-schema validity, retrieval recall@k, and an LLM-as-judge with a rubric whose own cost is reported apart from the pipeline's.
|
|
9
|
+
- **Diffs and CI gates**: `evalbar diff base.json head.json --fail-on-drop 0.02` names the regressed cases and exits 1 when quality drops (or cost rises past `--max-cost-increase`).
|
|
10
|
+
- **Reports**: a terminal table, a JSON run file, and a single self-contained HTML page (`evalbar html`).
|
|
11
|
+
- **Honest numbers**: a crashed case fails every scorer it applies to instead of disappearing; an unknown price is reported as unknown, never as $0.
|
|
12
|
+
|
|
13
|
+
Status: M1–M3 of the [roadmap](docs/roadmap.md) are done; version 0.1.0 is ready to release but not yet on PyPI. Design and comparison with promptfoo, DeepEval and Ragas: [docs/design.md](docs/design.md).
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/MilindDevX/evalbar && cd evalbar
|
|
19
|
+
uv venv --python 3.12 && uv pip install -e ".[dev]"
|
|
20
|
+
.venv/bin/evalbar run examples/quickstart/eval.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After the 0.1.0 release: `pip install evalbar`.
|
|
24
|
+
|
|
25
|
+
The quickstart needs no API key (LiteLLM's `mock_response` stands in for the model) and prints a score table, a usage table and the failing case with its reason. The run is saved under `runs/`; re-print it with `evalbar show runs/<file>.json`.
|
|
26
|
+
|
|
27
|
+
## Write an eval
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# my_eval.py
|
|
31
|
+
import json
|
|
32
|
+
|
|
33
|
+
from evalbar import Eval, completion, load_dataset, traced_tool
|
|
34
|
+
from evalbar.scorers import exact, json_schema, regex
|
|
35
|
+
|
|
36
|
+
@traced_tool # recorded in the case trace: args, result, latency, errors
|
|
37
|
+
def search(query: str) -> list[str]:
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def pipeline(case_input: dict) -> dict:
|
|
41
|
+
docs = search(case_input["question"])
|
|
42
|
+
reply = completion( # litellm.completion + tokens/latency/cost in the trace
|
|
43
|
+
model="groq/openai/gpt-oss-120b",
|
|
44
|
+
messages=[{"role": "user", "content": f"{docs}\n\n{case_input['question']}"}],
|
|
45
|
+
response_format={"type": "json_object"},
|
|
46
|
+
)
|
|
47
|
+
return json.loads(reply.choices[0].message.content)
|
|
48
|
+
|
|
49
|
+
my_eval = Eval(
|
|
50
|
+
name="support-bot",
|
|
51
|
+
pipeline=pipeline,
|
|
52
|
+
dataset=load_dataset("cases.jsonl"),
|
|
53
|
+
scorers=[
|
|
54
|
+
exact("label"), # output["label"] == expected["label"]
|
|
55
|
+
json_schema(SCHEMA), # output validates
|
|
56
|
+
regex("answer", r"^The user", must_match=False), # style rule
|
|
57
|
+
],
|
|
58
|
+
min_interval_s=2.0, # simple rate limit
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
evalbar run my_eval.py --out runs/baseline.json # add :name when the file defines several Evals
|
|
64
|
+
evalbar run my_eval.py --limit 3 # smoke test before spending quota
|
|
65
|
+
evalbar show runs/baseline.json
|
|
66
|
+
evalbar html runs/baseline.json # writes runs/baseline.html
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A case in `cases.jsonl`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{"id": "refund-01", "input": {"question": "How do I get a refund?"}, "expected": {"label": "billing"}, "metadata": {"split": "dev"}}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A scorer is any `(output, case) -> Score | None` function. Built-ins return `None` when the case has no expectation for them (for example `contains("secondary_themes")` on a case without `expected.secondary_themes`), so every scorer is averaged only over the cases it applies to.
|
|
76
|
+
|
|
77
|
+
`evalbar run` loads `.env` from the current folder if present (variables already set in the shell win); see `.env.example`.
|
|
78
|
+
|
|
79
|
+
## Retrieval and LLM-as-judge
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from evalbar.scorers import llm_judge, recall_at_k
|
|
83
|
+
|
|
84
|
+
recall_at_k(k=3) # share of expected["relevant"] ids in output["retrieved"][:3]
|
|
85
|
+
llm_judge(
|
|
86
|
+
"The answer is correct, supported by the context, and invents nothing.",
|
|
87
|
+
model="groq/openai/gpt-oss-120b",
|
|
88
|
+
pass_threshold=0.7,
|
|
89
|
+
expects="judge_me", # optional: only cases that carry this expected key
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The judge sees the rubric, the case input, the expected values and the output, and must reply `{"score": 0..1, "reason": "..."}`. Its calls are traced with role `judge`, so the report shows judge cost separately and `cost` stays the cost of the system under test. A judge that errors or replies with something unparseable fails the case with a `judge error` reason instead of being skipped.
|
|
94
|
+
|
|
95
|
+
[`examples/rag/`](examples/rag) is a tiny RAG pipeline: a keyword retriever over 8 documents of a fictional notes app, and an answer model. It runs offline by default; `EVALBAR_LIVE=1` uses Groq for answers and for the judge. Measured 2026-10-23, 8 questions:
|
|
96
|
+
|
|
97
|
+
| scorer | pass rate | what it caught |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| recall@2 | 7/8 | `money-back` ("Can I get my money back?") shares no words with the refund document |
|
|
100
|
+
| contains (expected fact in answer) | 4/8 | the same miss, plus 3 correct but terse answers ("Yes, the Business plan supports SSO with Okta." lacks "SAML") |
|
|
101
|
+
| judge (answered, faithful, threshold 0.7) | 7/8 | only `money-back`, where the model answered "I don't know" |
|
|
102
|
+
|
|
103
|
+
The keyword check penalised correct answers; the judge and recall@k agreed on the one real failure. The judge cost $0.00085, about 2× the pipeline's own $0.00039 for these 8 cases. The judge here is the same model as the pipeline, which can bias it toward its own answers; set `EVALBAR_JUDGE_MODEL` to use a different one.
|
|
104
|
+
|
|
105
|
+
## Compare runs and gate CI
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
evalbar diff runs/baseline.json runs/head.json # report only, exit 0
|
|
109
|
+
evalbar diff runs/baseline.json runs/head.json --fail-on-drop 0.02 # exit 1 if any pass rate drops > 2 points
|
|
110
|
+
evalbar diff runs/baseline.json runs/head.json --fail-on-drop 0 --scorer exact:label --max-cost-increase 0.2
|
|
111
|
+
evalbar diff runs/baseline.json runs/head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The diff compares only the cases both runs share, so adding or removing cases cannot fake a gain. For each scorer it shows the pass rate before and after and the ids of cases that regressed or got fixed (a case that newly crashes counts as a regression). It also shows cost, tokens and LLM latency per case.
|
|
115
|
+
|
|
116
|
+
Reversing the FeedbackOS comparison below (production prompt as the baseline, old prompt as the change) is a realistic regression, and the gate catches it:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
$ evalbar diff runs/feedbackos-v2.json runs/feedbackos-v1.json --fail-on-drop 0.02
|
|
120
|
+
FAIL: exact:sentiment dropped 6.0 pts (100.0% -> 94.0%), allowed 2.0; regressed: mixed-01, mixed-02, mixed-03
|
|
121
|
+
FAIL: theme+sentiment dropped 4.0 pts (96.0% -> 92.0%), allowed 2.0; regressed: mixed-01, mixed-02
|
|
122
|
+
FAIL: contains:secondary_themes dropped 100.0 pts (100.0% -> 0.0%), allowed 2.0; regressed: mixed-01, ..., mixed-08
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Forward (v1 to v2), the quality gate passes, but `--max-cost-increase 0.10` fails because v2 costs 13% more per item. The job surfaces the trade-off instead of hiding it.
|
|
126
|
+
|
|
127
|
+
GitHub Actions:
|
|
128
|
+
|
|
129
|
+
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs this repo's tests on Python 3.10–3.13. It also dogfoods the gate: it runs the offline quickstart and diffs it against the committed [`examples/quickstart/baseline.json`](examples/quickstart/baseline.json), so no API key is needed.
|
|
130
|
+
- [`examples/ci/evalbar-gate.yml`](examples/ci/evalbar-gate.yml) is a template to copy into your project. It runs your eval against a real model on pull requests, compares it with a committed baseline, writes the table to the job summary and uploads the run file.
|
|
131
|
+
|
|
132
|
+
## First real user: FeedbackOS's feedback classifier
|
|
133
|
+
|
|
134
|
+
FeedbackOS (Milind's feedback-triage SaaS) classifies customer feedback into a theme (`bug_problem`, `feature_request`, `usability_issue`, `question_support`, `praise`, `other`), a sentiment and a product area with `openai/gpt-oss-120b` on Groq. On 2026-10-12 its prompt changed: v2 added `secondary_themes` (praise inside a complaint is kept) and a stricter "mixed" sentiment rule.
|
|
135
|
+
|
|
136
|
+
[`examples/feedbackos/`](examples/feedbackos) reruns both prompt versions with the production settings:
|
|
137
|
+
|
|
138
|
+
- `prompts/v1.txt`, `prompts/v2.txt`: system prompts copied verbatim from FeedbackOS's git history (v1 = `9a896d9`, v2 = `59e4902`, the production prompt).
|
|
139
|
+
- `adapter.py`: a Python port of `classifyFeedback` (same user prompt, model, `temperature=0`, 512 max tokens, JSON mode, `reasoning_effort="low"`, short-text guard, product-area fallback).
|
|
140
|
+
- `cases.jsonl`: 50 synthetic, hand-labelled cases (8 bug, 8 feature request, 7 usability, 7 question, 7 praise, 5 other, 8 mixed praise+problem). No real user data.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
cp .env.example .env # then put your GROQ_API_KEY in .env with an editor
|
|
144
|
+
evalbar run examples/feedbackos/eval.py:v1 --out runs/feedbackos-v1.json
|
|
145
|
+
evalbar run examples/feedbackos/eval.py:v2 --out runs/feedbackos-v2.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Results
|
|
149
|
+
|
|
150
|
+
Measured 2026-10-22 on Groq, one run per prompt, 50 cases each, 49 LLM calls each (one case is under 5 words and skips the model, as in production). evalbar 0.1.0.dev0, LiteLLM 1.106.2. Raw run files are not committed; rerun both evals, then `evalbar diff runs/feedbackos-v1.json runs/feedbackos-v2.json --format markdown` regenerates the comparison.
|
|
151
|
+
|
|
152
|
+
| metric | v1 (single theme) | v2 (production) | change |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| theme accuracy (n=50) | 48/50 (96%) | 48/50 (96%) | 0 |
|
|
155
|
+
| sentiment accuracy (n=50) | 47/50 (94%) | **50/50 (100%)** | +6 pts |
|
|
156
|
+
| theme and sentiment both right (n=50) | 46/50 (92%) | **48/50 (96%)** | +4 pts |
|
|
157
|
+
| product-area accuracy (n=50) | 48/50 (96%) | 48/50 (96%) | 0 |
|
|
158
|
+
| praise kept in mixed feedback (n=8) | 0/8 | **8/8** | +8 cases |
|
|
159
|
+
| valid output schema | 50/50 | 50/50 | 0 |
|
|
160
|
+
| summary at most 15 words | 50/50 | 50/50 | 0 |
|
|
161
|
+
| prompt tokens per case | 778 | 929 | +19% |
|
|
162
|
+
| completion tokens per case | 107 | 118 | +10% |
|
|
163
|
+
| cost per 1,000 items (Groq list price) | $0.16 | $0.18 | +13% |
|
|
164
|
+
| LLM latency p50 / p95 | 0.90s / 1.83s | 0.80s / 1.63s | within noise |
|
|
165
|
+
|
|
166
|
+
What changed and what did not:
|
|
167
|
+
|
|
168
|
+
- **All of v2's gain is on mixed feedback** (praise next to a problem or request). On the 42 single-theme cases both prompts score the same. On the 8 mixed cases, v1 never keeps the praise (it has no `secondary_themes` field) and gets the sentiment wrong 3 times: `mixed-01` negative, `mixed-02` positive, `mixed-03` negative, all expected mixed. v2 gets all 8 right.
|
|
169
|
+
- **The price is a longer prompt**: +19% prompt tokens, +13% cost per item. On Groq's free tier the actual spend is $0; the list price for both runs together was $0.017 (96,591 tokens).
|
|
170
|
+
- **Shared misses** (both prompts, so not caused by the change):
|
|
171
|
+
- `other-01`: a SOC 2 / vendor-questionnaire request is classified `question_support`, although the prompt's own boundary rule says administrative requests are `other`. A real prompt-following gap.
|
|
172
|
+
- `other-05`: "ok see attached" (under 5 words) skips the model and gets product area `Dashboard`, because FeedbackOS's short-text guard uses the first product area instead of the `Other` fallback. A real FeedbackOS bug, found by this eval.
|
|
173
|
+
- `feat-03` (webhooks: `API` vs `Integrations`) and `mixed-03` (a broken "Remember me" box: `bug_problem` vs `usability_issue`) are arguably label ambiguity rather than model errors.
|
|
174
|
+
|
|
175
|
+
Caveats: 50 synthetic cases written by the evalbar author and labelled to the v2 contract, so one case is 2 points and these numbers say nothing about production accuracy. `praise kept` measures a field v1 cannot produce, so it shows the contract change, not v1 failing at its own job. Each prompt was run once; `gpt-oss-120b` at temperature 0 is not guaranteed deterministic.
|
|
176
|
+
|
|
177
|
+
What the CLI prints for v2 (trimmed):
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
evalbar run: feedbackos-v2 50 cases, 0 errors, started 2026-10-22T13:10:02+00:00
|
|
181
|
+
Scores
|
|
182
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓
|
|
183
|
+
┃ scorer ┃ cases ┃ passed ┃ pass rate ┃ mean ┃
|
|
184
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━┩
|
|
185
|
+
│ exact:theme │ 50 │ 48 │ 96.0% │ 0.960 │
|
|
186
|
+
│ exact:sentiment │ 50 │ 50 │ 100.0% │ 1.000 │
|
|
187
|
+
│ exact:product_area │ 50 │ 48 │ 96.0% │ 0.960 │
|
|
188
|
+
│ theme+sentiment │ 50 │ 48 │ 96.0% │ 0.960 │
|
|
189
|
+
│ contains:secondary_themes │ 8 │ 8 │ 100.0% │ 1.000 │
|
|
190
|
+
│ valid_output │ 50 │ 50 │ 100.0% │ 1.000 │
|
|
191
|
+
│ summary<=15 words │ 50 │ 50 │ 100.0% │ 1.000 │
|
|
192
|
+
└───────────────────────────┴───────┴────────┴───────────┴───────┘
|
|
193
|
+
Usage
|
|
194
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
|
|
195
|
+
┃ metric ┃ value ┃
|
|
196
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
|
|
197
|
+
│ LLM calls │ 49 │
|
|
198
|
+
│ prompt tokens │ 46,454 │
|
|
199
|
+
│ completion tokens │ 5,887 │
|
|
200
|
+
│ cost (list price) │ $0.009233 │
|
|
201
|
+
│ cost per case │ $0.000185 │
|
|
202
|
+
│ case latency p50 / p95 │ 0.80s / 1.62s │
|
|
203
|
+
│ LLM latency p50 / p95 │ 0.80s / 1.63s │
|
|
204
|
+
└────────────────────────┴───────────────┘
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Releasing (maintainer)
|
|
208
|
+
|
|
209
|
+
`.github/workflows/release.yml` builds, runs `twine check --strict` and publishes to PyPI with trusted publishing when a `v*` tag matching `pyproject.toml`'s version is pushed. One-time setup: add a pending publisher on pypi.org (project `evalbar`, owner `MilindDevX`, repo `evalbar`, workflow `release.yml`, environment `pypi`) and create the `pypi` environment in the repo settings.
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
uv venv --python 3.12 && uv pip install -e ".[dev]"
|
|
215
|
+
.venv/bin/python -m pytest -q
|
|
216
|
+
.venv/bin/ruff check .
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Costs come from the price LiteLLM computes during the call (`response_cost`), because `completion_cost()` on a Groq response looks the model up under OpenAI and fails; see [design decision 4](docs/design.md#design-decisions).
|
|
220
|
+
|
|
221
|
+
Tests run offline: LiteLLM's `mock_response` exercises the real `completion()` path, and LiteLLM's bundled price table is used (`LITELLM_LOCAL_MODEL_COST_MAP=True`) so costs are reproducible and tied to the LiteLLM version recorded in each run file.
|
|
222
|
+
|
|
223
|
+
LiteLLM 1.82.7 and 1.82.8 were compromised releases on PyPI (March 2026); the dependency range excludes them.
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|