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.
Files changed (57) hide show
  1. {evalstone-0.2.0 → evalstone-0.2.1}/CHANGELOG.md +19 -3
  2. {evalstone-0.2.0 → evalstone-0.2.1}/PKG-INFO +1 -1
  3. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/__init__.py +1 -1
  4. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/center.py +8 -6
  5. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/comparison.py +7 -4
  6. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/cli.py +4 -0
  7. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/score.py +2 -1
  8. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_center.py +39 -0
  9. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_cli.py +15 -0
  10. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_docs.py +14 -0
  11. {evalstone-0.2.0 → evalstone-0.2.1}/.gitignore +0 -0
  12. {evalstone-0.2.0 → evalstone-0.2.1}/LICENSE +0 -0
  13. {evalstone-0.2.0 → evalstone-0.2.1}/README.md +0 -0
  14. {evalstone-0.2.0 → evalstone-0.2.1}/justfile +0 -0
  15. {evalstone-0.2.0 → evalstone-0.2.1}/pyproject.toml +0 -0
  16. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/__main__.py +0 -0
  17. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/_git.py +0 -0
  18. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/_json.py +0 -0
  19. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/__init__.py +0 -0
  20. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/_base.py +0 -0
  21. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/deterministic.py +0 -0
  22. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/human.py +0 -0
  23. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/judge.py +0 -0
  24. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/rag.py +0 -0
  25. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/safety.py +0 -0
  26. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/checks/trajectory.py +0 -0
  27. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/config.py +0 -0
  28. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/dataset.py +0 -0
  29. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/gate.py +0 -0
  30. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/mcp_server.py +0 -0
  31. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/output.py +0 -0
  32. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/__init__.py +0 -0
  33. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/_http.py +0 -0
  34. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/anthropic_.py +0 -0
  35. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/gemini.py +0 -0
  36. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/providers/openai_compat.py +0 -0
  37. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/pytest_plugin.py +0 -0
  38. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/run.py +0 -0
  39. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/suite.py +0 -0
  40. {evalstone-0.2.0 → evalstone-0.2.1}/src/evalstone/target.py +0 -0
  41. {evalstone-0.2.0 → evalstone-0.2.1}/tests/conftest.py +0 -0
  42. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_action.py +0 -0
  43. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_config.py +0 -0
  44. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_dataset.py +0 -0
  45. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_deterministic.py +0 -0
  46. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_human.py +0 -0
  47. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_judge.py +0 -0
  48. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_judge_switching.py +0 -0
  49. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_mcp.py +0 -0
  50. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_providers.py +0 -0
  51. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_pytest_plugin.py +0 -0
  52. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_rag_and_comparison.py +0 -0
  53. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_safety.py +0 -0
  54. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_score_and_checks.py +0 -0
  55. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_suite_run_gate.py +0 -0
  56. {evalstone-0.2.0 → evalstone-0.2.1}/tests/test_target.py +0 -0
  57. {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 and prompt-target calls through the center's model proxy, so no provider
11
- key lives on the machine running the suite; the project's judge settings pick the
12
- model (per-check overrides, then the default), `--judge-model` still wins.
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.0
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.0"
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, the library never holds a provider key: judge (and prompt-target) calls go
4
- through the center's model proxy, which resolves the model from the project's judge
5
- settings and dials the provider with the organization's key server-side. The judge
6
- cache lives in the center (one verdict serves every machine), ``human`` checks look
7
- labels up there, and every ``evalstone run`` pushes its report. ``providers:`` and
8
- ``judges:`` in the YAML remain the standalone path.
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(ctx: Context, sample_input: str, a: str, b: str) -> tuple[str, str, float, float]:
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
- judge_model_for(ctx, key, model),
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(ctx.judge_model or model))
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``. The center-backed cache arrives with Phase 3."""
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