evalstone 0.2.0__tar.gz → 0.2.1__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.
- {evalstone-0.2.0 → evalstone-0.2.1}/CHANGELOG.md +19 -3
- {evalstone-0.2.0 → evalstone-0.2.1}/PKG-INFO +1 -1
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/__init__.py +1 -1
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/center.py +8 -6
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/comparison.py +7 -4
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/cli.py +4 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/score.py +2 -1
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_center.py +39 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_cli.py +15 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_docs.py +14 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/.gitignore +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/LICENSE +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/README.md +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/justfile +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/pyproject.toml +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/__main__.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/_git.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/_json.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/__init__.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/_base.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/deterministic.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/human.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/judge.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/rag.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/safety.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/trajectory.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/config.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/dataset.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/gate.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/mcp_server.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/output.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/__init__.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/_http.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/anthropic_.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/gemini.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/openai_compat.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/pytest_plugin.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/run.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/suite.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/target.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/conftest.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_action.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_config.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_dataset.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_deterministic.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_human.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_judge.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_judge_switching.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_mcp.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_providers.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_pytest_plugin.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_rag_and_comparison.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_safety.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_score_and_checks.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_suite_run_gate.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_target.py +0 -0
- {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_trajectory.py +0 -0
|
@@ -2,14 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `python/evalstone`. Semantic versioning; tags `lib-vX.Y.Z`.
|
|
4
4
|
|
|
5
|
+
## 0.2.1 — 2026-10-14
|
|
6
|
+
|
|
7
|
+
Attach fixes found by the docs audit.
|
|
8
|
+
|
|
9
|
+
- `comparison.pairwise` built its check version (and so its cache key) from
|
|
10
|
+
`--judge-model` or the check's own `model:`, ignoring the attached project's
|
|
11
|
+
per-check override — the call itself already honoured it. Both now resolve through
|
|
12
|
+
the same precedence (`--judge-model` > center override > `model:` > default), so a
|
|
13
|
+
verdict cached under the override is found again and a changed override invalidates
|
|
14
|
+
the cache.
|
|
15
|
+
- Docstrings and this changelog no longer claim prompt targets are proxied when
|
|
16
|
+
attached; they never were (`providers:` + env keys serve the system under test).
|
|
17
|
+
- Known limitation, documented: when attached, `--judge-model some-typo/model` is
|
|
18
|
+
not validated up front — the center only rejects the provider at the first judge
|
|
19
|
+
call, which surfaces as an errored score. Standalone runs still fail fast.
|
|
20
|
+
|
|
5
21
|
## 0.2.0 — 2026-10-14
|
|
6
22
|
|
|
7
23
|
Attach a project to a center.
|
|
8
24
|
|
|
9
25
|
- `center:` in `evalstone.yaml` (`url`, `project`, `api_key_env` — an `es_…` ingest key)
|
|
10
|
-
routes judge
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
routes judge calls through the center's model proxy, so no judge key lives on the
|
|
27
|
+
machine running the suite; the project's judge settings pick the model (per-check
|
|
28
|
+
overrides, then the default), `--judge-model` still wins.
|
|
13
29
|
- Judge verdicts are cached in the center (`CenterCache`); a center hiccup is a cache
|
|
14
30
|
miss, never a failed run.
|
|
15
31
|
- `human` checks of an attached project look labels up in the center (`labels: center`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evalstone
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: The plug-and-play evals layer for LLM-based projects: datasets, checks, suites, runs and gates.
|
|
5
5
|
Project-URL: Homepage, https://evalstone.com
|
|
6
6
|
Project-URL: Documentation, https://evalstone.com/docs
|
|
@@ -8,7 +8,7 @@ import types
|
|
|
8
8
|
from importlib import import_module
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
|
-
__version__ = "0.2.
|
|
11
|
+
__version__ = "0.2.1"
|
|
12
12
|
|
|
13
13
|
# Exports resolve on first access (PEP 562) so that importing the package — which
|
|
14
14
|
# the pytest plugin does in every pytest session on the interpreter — costs
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Attach a project to an Evalstone center (``center:`` in evalstone.yaml).
|
|
2
2
|
|
|
3
|
-
Attached,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
``
|
|
3
|
+
Attached, judge calls go through the center's model proxy, which resolves the model
|
|
4
|
+
from the project's judge settings and dials the provider with the organization's key
|
|
5
|
+
server-side, so no judge key lives on the machine running the suite. Prompt targets
|
|
6
|
+
(the system under test) are NOT proxied: they keep using the local ``providers:`` and
|
|
7
|
+
their env keys. The judge cache lives in the center (one verdict serves every
|
|
8
|
+
machine), ``human`` checks look labels up there, and every ``evalstone run`` pushes
|
|
9
|
+
its report. ``judges:`` in the YAML is ignored while attached; ``providers:`` still
|
|
10
|
+
serves prompt targets.
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
from __future__ import annotations
|
|
@@ -66,10 +66,12 @@ def pairwise(
|
|
|
66
66
|
identity = {"v": 1, "rubric": rubric, "both_orders": both_orders}
|
|
67
67
|
unpriced: list[str] = [] # models of this sample's calls that the price table lacks
|
|
68
68
|
|
|
69
|
-
def ask(
|
|
69
|
+
def ask(
|
|
70
|
+
ctx: Context, chosen: str | None, sample_input: str, a: str, b: str
|
|
71
|
+
) -> tuple[str, str, float, float]:
|
|
70
72
|
user = USER.format(input=sample_input, a=a, b=b)
|
|
71
73
|
resp = ctx.providers.complete(
|
|
72
|
-
|
|
74
|
+
chosen,
|
|
73
75
|
{
|
|
74
76
|
"system": SYSTEM.format(rubric=rubric.strip()),
|
|
75
77
|
"messages": [ChatMessage(role="user", content=user)],
|
|
@@ -85,8 +87,9 @@ def pairwise(
|
|
|
85
87
|
if ctx.providers is None:
|
|
86
88
|
return Score.errored("pairwise: no providers configured")
|
|
87
89
|
unpriced.clear()
|
|
90
|
+
chosen = judge_model_for(ctx, key, model) # resolved once: the version AND the call
|
|
88
91
|
try:
|
|
89
|
-
resolved = "/".join(ctx.providers.split(
|
|
92
|
+
resolved = "/".join(ctx.providers.split(chosen))
|
|
90
93
|
except ProviderError as exc:
|
|
91
94
|
return Score.errored(str(exc))
|
|
92
95
|
version = sha256({**identity, "model": resolved})[:16]
|
|
@@ -111,7 +114,7 @@ def pairwise(
|
|
|
111
114
|
if both_orders:
|
|
112
115
|
orders.append(("base_first", base.text, output.text))
|
|
113
116
|
for label, a, b in orders:
|
|
114
|
-
winner, reason, c, ms = ask(ctx, text, a, b)
|
|
117
|
+
winner, reason, c, ms = ask(ctx, chosen, text, a, b)
|
|
115
118
|
cost += c
|
|
116
119
|
latency += ms
|
|
117
120
|
candidate_is_a = label == "cand_first"
|
|
@@ -145,6 +145,10 @@ def run(
|
|
|
145
145
|
if judge_model is not None:
|
|
146
146
|
try:
|
|
147
147
|
registry = cfg.provider_registry()
|
|
148
|
+
except CenterError as exc: # attached without a key: the center, not the flag
|
|
149
|
+
err.print(f"[red]{escape(str(exc))}[/red]")
|
|
150
|
+
raise typer.Exit(2) from None
|
|
151
|
+
try:
|
|
148
152
|
registry.get(registry.split(judge_model)[0])
|
|
149
153
|
except ProviderError as exc:
|
|
150
154
|
err.print(f"[red]--judge-model: {escape(str(exc))}[/red]")
|
|
@@ -95,7 +95,8 @@ class ScoreCache(Protocol):
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
class MemoryCache:
|
|
98
|
-
"""An in-process ``ScoreCache
|
|
98
|
+
"""An in-process ``ScoreCache`` (the standalone default; attached projects use
|
|
99
|
+
``evalstone.center.CenterCache``)."""
|
|
99
100
|
|
|
100
101
|
def __init__(self) -> None:
|
|
101
102
|
self._data: dict[str, Score] = {}
|
|
@@ -312,6 +312,45 @@ def test_pairwise_honours_the_center_override(cfg):
|
|
|
312
312
|
assert json.loads(calls.calls[0].request.content)["model"] == "other/big"
|
|
313
313
|
|
|
314
314
|
|
|
315
|
+
@respx.mock
|
|
316
|
+
def test_pairwise_cache_key_follows_the_center_override(cfg):
|
|
317
|
+
"""The check version (and so the cache key) is built from the same resolved model as
|
|
318
|
+
the call: a verdict cached under the override is hit again, a changed override is a
|
|
319
|
+
different version and misses (regression: the key used ``ctx.judge_model or model``)."""
|
|
320
|
+
from evalstone.checks.comparison import pairwise
|
|
321
|
+
from evalstone.score import MemoryCache
|
|
322
|
+
|
|
323
|
+
calls = respx.post(f"{P}/model/complete").mock(
|
|
324
|
+
return_value=_verdict(text='{"winner": "A", "reason": "clearer"}')
|
|
325
|
+
)
|
|
326
|
+
client = CenterClient(cfg)
|
|
327
|
+
providers = CenterProviders(client, default_model="judge/small")
|
|
328
|
+
extra = {"center_client": client, "judge_models": {"better": "other/big"}}
|
|
329
|
+
cache = MemoryCache()
|
|
330
|
+
check = pairwise(
|
|
331
|
+
"better", "Which is clearer?", baseline={"s": "baseline answer"}, model="judge/small"
|
|
332
|
+
)
|
|
333
|
+
sample, output = Sample(id="s", input="q"), Output(value="candidate")
|
|
334
|
+
|
|
335
|
+
ctx = Context(providers=providers, extra=extra, cache=cache)
|
|
336
|
+
check.evaluate(sample, output, ctx)
|
|
337
|
+
filled = calls.call_count # both orders were asked once
|
|
338
|
+
assert len(cache._data) == 1
|
|
339
|
+
key_before = next(iter(cache._data))
|
|
340
|
+
assert check.evaluate(sample, output, ctx).raw["cached"] is True
|
|
341
|
+
assert calls.call_count == filled # served from the cache
|
|
342
|
+
|
|
343
|
+
ctx_other = Context(
|
|
344
|
+
providers=providers,
|
|
345
|
+
extra={**extra, "judge_models": {"better": "other/small"}},
|
|
346
|
+
cache=cache,
|
|
347
|
+
)
|
|
348
|
+
assert check.evaluate(sample, output, ctx_other).raw.get("cached") is not True
|
|
349
|
+
assert calls.call_count > filled # a changed override is a different version → miss
|
|
350
|
+
assert json.loads(calls.calls[-1].request.content)["model"] == "other/small"
|
|
351
|
+
assert len(cache._data) == 2 and key_before in cache._data
|
|
352
|
+
|
|
353
|
+
|
|
315
354
|
@respx.mock
|
|
316
355
|
def test_prompt_targets_stay_local_when_attached(tmp_path, monkeypatch):
|
|
317
356
|
from evalstone.providers import Providers
|
|
@@ -143,3 +143,18 @@ def test_show_markdown(in_dir):
|
|
|
143
143
|
"### Evalstone · `nightly` — ✅ gate passed"
|
|
144
144
|
)
|
|
145
145
|
assert "| `must_include` |" in res.output
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_missing_center_key_is_reported_as_the_center_not_the_flag(in_dir, monkeypatch):
|
|
149
|
+
"""Attached without EVALSTONE_API_KEY, ``--judge-model`` must not blame the flag."""
|
|
150
|
+
monkeypatch.delenv("EVALSTONE_API_KEY", raising=False)
|
|
151
|
+
runner.invoke(app, ["init", "--project", "demo"])
|
|
152
|
+
cfg = in_dir / "evalstone.yaml"
|
|
153
|
+
cfg.write_text(
|
|
154
|
+
cfg.read_text()
|
|
155
|
+
+ "center:\n url: http://center.test\n project: 11111111-2222-4333-8444-555555555555\n"
|
|
156
|
+
)
|
|
157
|
+
res = runner.invoke(app, ["run", "nightly", "--judge-model", "openai/gpt-x"])
|
|
158
|
+
assert res.exit_code == 2
|
|
159
|
+
assert "EVALSTONE_API_KEY" in res.output
|
|
160
|
+
assert "--judge-model" not in res.output
|
|
@@ -51,3 +51,17 @@ def test_every_action_snippet_uses_the_same_ref():
|
|
|
51
51
|
)
|
|
52
52
|
)
|
|
53
53
|
assert refs == {"action-v0"}, f"action refs in the docs: {sorted(refs)}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_site_changelog_is_generated_from_the_library_changelog():
|
|
57
|
+
"""The site's Changelog page is `scripts/sync-changelog.mjs` output committed next to
|
|
58
|
+
the docs; a release that bumps CHANGELOG.md without re-running it is caught here."""
|
|
59
|
+
changelog = (SITE_DOCS.parents[2].parent / "python" / "evalstone" / "CHANGELOG.md").read_text(
|
|
60
|
+
encoding="utf-8"
|
|
61
|
+
)
|
|
62
|
+
page = (SITE_DOCS / "evalstone" / "changelog.mdx").read_text(encoding="utf-8")
|
|
63
|
+
body = changelog[changelog.index("\n## ") + 1 :].rstrip()
|
|
64
|
+
assert "GENERATED by scripts/sync-changelog.mjs" in page
|
|
65
|
+
assert page.rstrip().endswith(body), (
|
|
66
|
+
"run `npm run build` (or scripts/sync-changelog.mjs) in the site"
|
|
67
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|