evalcore 2.4.1__tar.gz → 2.4.2__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 (76) hide show
  1. {evalcore-2.4.1 → evalcore-2.4.2}/CHANGELOG.md +36 -1
  2. {evalcore-2.4.1 → evalcore-2.4.2}/PKG-INFO +1 -1
  3. {evalcore-2.4.1 → evalcore-2.4.2}/pyproject.toml +1 -1
  4. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/graders/judge.py +21 -1
  5. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_judge.py +56 -7
  6. {evalcore-2.4.1 → evalcore-2.4.2}/uv.lock +1 -1
  7. {evalcore-2.4.1 → evalcore-2.4.2}/.github/workflows/ci.yml +0 -0
  8. {evalcore-2.4.1 → evalcore-2.4.2}/.github/workflows/publish.yml +0 -0
  9. {evalcore-2.4.1 → evalcore-2.4.2}/.gitignore +0 -0
  10. {evalcore-2.4.1 → evalcore-2.4.2}/.pre-commit-config.yaml +0 -0
  11. {evalcore-2.4.1 → evalcore-2.4.2}/LICENSE +0 -0
  12. {evalcore-2.4.1 → evalcore-2.4.2}/README.md +0 -0
  13. {evalcore-2.4.1 → evalcore-2.4.2}/docs/design.md +0 -0
  14. {evalcore-2.4.1 → evalcore-2.4.2}/examples/__init__.py +0 -0
  15. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/README.md +0 -0
  16. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/__init__.py +0 -0
  17. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/adapter.py +0 -0
  18. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/datasets/support_reply/v1/cases/angry_complaint.yaml +0 -0
  19. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/datasets/support_reply/v1/cases/billing_question.yaml +0 -0
  20. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/datasets/support_reply/v1/cases/double_charge.yaml +0 -0
  21. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/datasets/support_reply/v1/cases/refund_request.yaml +0 -0
  22. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/fixtures/support_reply_judge.yaml +0 -0
  23. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/fixtures/support_reply_pairwise.yaml +0 -0
  24. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/fixtures/support_reply_replay.yaml +0 -0
  25. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/graders.py +0 -0
  26. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/run_eval.py +0 -0
  27. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/suite.yaml +0 -0
  28. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/tests/__init__.py +0 -0
  29. {evalcore-2.4.1 → evalcore-2.4.2}/examples/quickstart/tests/test_quickstart.py +0 -0
  30. {evalcore-2.4.1 → evalcore-2.4.2}/justfile +0 -0
  31. {evalcore-2.4.1 → evalcore-2.4.2}/pyrightconfig.json +0 -0
  32. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/__init__.py +0 -0
  33. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/adapters/__init__.py +0 -0
  34. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/adapters/base.py +0 -0
  35. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/adapters/env.py +0 -0
  36. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/adapters/http.py +0 -0
  37. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/adapters/replay.py +0 -0
  38. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/cli.py +0 -0
  39. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/compare.py +0 -0
  40. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/errors.py +0 -0
  41. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/graders/__init__.py +0 -0
  42. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/graders/base.py +0 -0
  43. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/graders/classification.py +0 -0
  44. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/graders/deterministic.py +0 -0
  45. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/graders/numeric.py +0 -0
  46. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/loader.py +0 -0
  47. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/models.py +0 -0
  48. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/pairwise.py +0 -0
  49. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/py.typed +0 -0
  50. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/rating.py +0 -0
  51. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/refs.py +0 -0
  52. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/report.py +0 -0
  53. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/reporters/__init__.py +0 -0
  54. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/reporters/base.py +0 -0
  55. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/reporters/html.py +0 -0
  56. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/reporters/markdown.py +0 -0
  57. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/retry.py +0 -0
  58. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/runner.py +0 -0
  59. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/store.py +0 -0
  60. {evalcore-2.4.1 → evalcore-2.4.2}/src/evalcore/sweep.py +0 -0
  61. {evalcore-2.4.1 → evalcore-2.4.2}/tests/__init__.py +0 -0
  62. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_adapters.py +0 -0
  63. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_cli.py +0 -0
  64. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_edge_cases.py +0 -0
  65. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_judge_extra.py +0 -0
  66. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_live_clients.py +0 -0
  67. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_pairwise_extra.py +0 -0
  68. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_rating.py +0 -0
  69. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_rating_server.py +0 -0
  70. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_reporters.py +0 -0
  71. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_retry.py +0 -0
  72. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_runner.py +0 -0
  73. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_store.py +0 -0
  74. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_sweep_pairwise.py +0 -0
  75. {evalcore-2.4.1 → evalcore-2.4.2}/tests/test_unit.py +0 -0
  76. {evalcore-2.4.1 → evalcore-2.4.2}/uv.toml +0 -0
@@ -6,6 +6,40 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.4.2] - 2026-08-09
10
+
11
+ Every `llm_as_judge` row now describes what it measures.
12
+
13
+ Shipped as a patch. It changes what published rows contain, so the 1.0.0
14
+ policy would call it a minor; it goes out as 2.4.2 as a deliberate exception,
15
+ alongside 2.4.1's judge-pin fix that it completes. No schema change - the
16
+ columns already exist and were empty.
17
+
18
+ ### Fixed
19
+ - A judge's dimension row (`<grader>.<dimension>`) carried no judge
20
+ information at all: no `judges.*`, and `judge_scale` of 0. So `0.400` on a
21
+ row could not say who scored it, on what scale, or that the raw number was
22
+ 2 out of 5 - the scale lived only on `<grader>.overall`, and recovering the
23
+ raw point meant joining back to it.
24
+
25
+ Each dimension row now carries the verdict for the dimension it measures:
26
+ `judges.name`, `judges.version`, `judges.rationale`, `judge_scale`, a
27
+ `judges.points` **scoped to that dimension**, and a `judges.score` that is
28
+ that judge's number for it.
29
+
30
+ `<grader>.overall` is unchanged and still carries the full points map.
31
+
32
+ This establishes one invariant across every judge row, dimension rows
33
+ included: `value == mean(judges.score)`. With one judge they are equal;
34
+ with a panel, `value` is the mean and `judges.score` shows the spread - so
35
+ per-dimension disagreement is readable from the dimension row instead of
36
+ by unpacking the map on `.overall`.
37
+
38
+ **Upgrading:** `notEmpty(judges.name)` now matches every judge row rather
39
+ than only `<grader>.overall`, so a six-case run goes from 12 judged rows to
40
+ 48. Anything aggregating over judges must filter to `.overall` or it
41
+ multiplies by the dimension count. That is a change in results, not an error.
42
+
9
43
  ## [2.4.1] - 2026-08-09
10
44
 
11
45
  The judge's identity is now in its provenance pin.
@@ -278,7 +312,8 @@ by semantic versioning: a breaking change to either means a 2.0.
278
312
  rating + ranking with judge agreement, Markdown/HTML reporters, JSON +
279
313
  column-store outbox, and content-hash provenance.
280
314
 
281
- [Unreleased]: https://github.com/scottpmiller/evalcore/compare/2.4.1...HEAD
315
+ [Unreleased]: https://github.com/scottpmiller/evalcore/compare/2.4.2...HEAD
316
+ [2.4.2]: https://github.com/scottpmiller/evalcore/compare/2.4.1...2.4.2
282
317
  [2.4.1]: https://github.com/scottpmiller/evalcore/compare/2.4.0...2.4.1
283
318
  [2.4.0]: https://github.com/scottpmiller/evalcore/compare/2.3.0...2.4.0
284
319
  [2.3.0]: https://github.com/scottpmiller/evalcore/compare/2.2.0...2.3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evalcore
3
- Version: 2.4.1
3
+ Version: 2.4.2
4
4
  Summary: A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs.
5
5
  Project-URL: Homepage, https://github.com/scottpmiller/evalcore
6
6
  Project-URL: Repository, https://github.com/scottpmiller/evalcore
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "evalcore"
3
- version = "2.4.1"
3
+ version = "2.4.2"
4
4
  description = "A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -579,6 +579,14 @@ class RubricJudge:
579
579
 
580
580
  # Panel mean per dimension (identical to the single judge's value
581
581
  # when there is only one).
582
+ #
583
+ # Each dimension row carries the verdict for the dimension it
584
+ # measures, not the whole one: points scoped to this key, and a score
585
+ # that is this judge's number for it. That makes `value ==
586
+ # mean(judges.score)` hold on every judge row, so per-dimension
587
+ # disagreement is readable without unpacking the map on `.overall`.
588
+ # Carrying the full map here would put two irrelevant numbers on a
589
+ # row and invite reading the wrong one.
582
590
  panel_dims: list[float] = []
583
591
  for key, _ in self.dimensions:
584
592
  per_judge = [
@@ -588,7 +596,19 @@ class RubricJudge:
588
596
  value = sum(present) / len(present) if present else None
589
597
  if value is not None:
590
598
  panel_dims.append(value)
591
- out.append(score(f'{self.name}.{key}', value))
599
+ dim_details = [
600
+ models.JudgeDetail(
601
+ key=judge['key'],
602
+ version=judge['judge_version'],
603
+ rationale=rationales.get(judge['key']),
604
+ points={key: raw.get(judge['key'], {}).get(key)},
605
+ overall=self._normalize(
606
+ raw.get(judge['key'], {}).get(key)
607
+ ),
608
+ )
609
+ for judge in active
610
+ ]
611
+ out.append(score(f'{self.name}.{key}', value, judges=dim_details))
592
612
 
593
613
  overall = sum(panel_dims) / len(panel_dims) if panel_dims else None
594
614
  out.append(
@@ -59,13 +59,62 @@ class RubricJudgeTests(unittest.TestCase):
59
59
  # raw 1..scale points are retained, not just the normalized mean
60
60
  self.assertEqual(jd.points, {'clarity': 5, 'specificity': 4})
61
61
  self.assertAlmostEqual(jd.overall, 0.9)
62
- # only the overall score carries the breakdown; dimensions do not
63
- dims = [
64
- s
65
- for s in _grade(grader, case, output)
66
- if s.metric == 'llm_judge.clarity'
67
- ]
68
- self.assertEqual(dims[0].judges, [])
62
+ # the overall score carries the whole breakdown
63
+ self.assertEqual(jd.points, {'clarity': 5, 'specificity': 4})
64
+
65
+ def test_a_dimension_row_carries_its_own_verdict(self):
66
+ """Every judge row describes what that row measures.
67
+
68
+ The dimension row used to carry nothing, so `0.8` alone could not say
69
+ who scored it, on what scale, or that the raw number was 4.
70
+ """
71
+ grader = self._grader(
72
+ {
73
+ 'Great subject': {
74
+ 'scores': {'clarity': 5, 'specificity': 4},
75
+ 'rationale': 'Clear and specific enough.',
76
+ }
77
+ }
78
+ )
79
+ scores = _grade(
80
+ grader,
81
+ models.Case(id='c1'),
82
+ models.Output(fields={'text': 'Great subject'}),
83
+ )
84
+ dim = next(s for s in scores if s.metric == 'llm_judge.specificity')
85
+ self.assertEqual(len(dim.judges), 1)
86
+ jd = dim.judges[0]
87
+ # Scoped: only the point this row is about.
88
+ self.assertEqual(jd.points, {'specificity': 4})
89
+ self.assertAlmostEqual(jd.overall, 0.8)
90
+ self.assertEqual(jd.rationale, 'Clear and specific enough.')
91
+
92
+ def test_value_is_the_mean_of_the_judge_scores(self):
93
+ """The invariant that holds on every judge row, dimensions included:
94
+ value == mean(judges.score)."""
95
+ grader = self._grader(
96
+ {'X': {'scores': {'clarity': 5, 'specificity': 2}}}
97
+ )
98
+ scores = _grade(
99
+ grader, models.Case(id='c'), models.Output(fields={'text': 'X'})
100
+ )
101
+ judged = [s for s in scores if s.judges and s.value is not None]
102
+ self.assertTrue(judged)
103
+ for s in judged:
104
+ mean = sum(j.overall for j in s.judges) / len(s.judges)
105
+ self.assertAlmostEqual(s.value, mean, msg=s.metric)
106
+
107
+ def test_the_overall_row_keeps_the_full_map(self):
108
+ grader = self._grader(
109
+ {'X': {'scores': {'clarity': 5, 'specificity': 4}}}
110
+ )
111
+ scores = _grade(
112
+ grader, models.Case(id='c'), models.Output(fields={'text': 'X'})
113
+ )
114
+ overall = next(s for s in scores if s.metric == 'llm_judge.overall')
115
+ self.assertEqual(
116
+ overall.judges[0].points, {'clarity': 5, 'specificity': 4}
117
+ )
69
118
 
70
119
  def test_different_content_gets_different_scores(self):
71
120
  grader = self._grader(
@@ -188,7 +188,7 @@ wheels = [
188
188
 
189
189
  [[package]]
190
190
  name = "evalcore"
191
- version = "2.4.1"
191
+ version = "2.4.2"
192
192
  source = { editable = "." }
193
193
  dependencies = [
194
194
  { name = "pydantic" },
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