evalsi 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.
Files changed (183) hide show
  1. evalsi-0.1.0/.gitignore +30 -0
  2. evalsi-0.1.0/PKG-INFO +62 -0
  3. evalsi-0.1.0/README.md +27 -0
  4. evalsi-0.1.0/pyproject.toml +66 -0
  5. evalsi-0.1.0/src/evalsi/__init__.py +77 -0
  6. evalsi-0.1.0/src/evalsi/__main__.py +7 -0
  7. evalsi-0.1.0/src/evalsi/_version.py +1 -0
  8. evalsi-0.1.0/src/evalsi/analytics.py +325 -0
  9. evalsi-0.1.0/src/evalsi/auth.py +458 -0
  10. evalsi-0.1.0/src/evalsi/cli.py +745 -0
  11. evalsi-0.1.0/src/evalsi/cli_analytics.py +105 -0
  12. evalsi-0.1.0/src/evalsi/cli_annotate.py +346 -0
  13. evalsi-0.1.0/src/evalsi/cli_auth.py +422 -0
  14. evalsi-0.1.0/src/evalsi/cli_credentials.py +53 -0
  15. evalsi-0.1.0/src/evalsi/cli_flywheel.py +125 -0
  16. evalsi-0.1.0/src/evalsi/cli_guardrails.py +143 -0
  17. evalsi-0.1.0/src/evalsi/cli_mcp.py +64 -0
  18. evalsi-0.1.0/src/evalsi/cli_plugins.py +141 -0
  19. evalsi-0.1.0/src/evalsi/cli_rewards.py +131 -0
  20. evalsi-0.1.0/src/evalsi/cli_sources.py +225 -0
  21. evalsi-0.1.0/src/evalsi/cli_training.py +197 -0
  22. evalsi-0.1.0/src/evalsi/client.py +719 -0
  23. evalsi-0.1.0/src/evalsi/compare.py +100 -0
  24. evalsi-0.1.0/src/evalsi/convert.py +476 -0
  25. evalsi-0.1.0/src/evalsi/datasets.py +216 -0
  26. evalsi-0.1.0/src/evalsi/evaluator.py +253 -0
  27. evalsi-0.1.0/src/evalsi/harness/__init__.py +1 -0
  28. evalsi-0.1.0/src/evalsi/harness/v1alpha1/__init__.py +1 -0
  29. evalsi-0.1.0/src/evalsi/harness/v1alpha1/harness_pb2.py +69 -0
  30. evalsi-0.1.0/src/evalsi/harness/v1alpha1/harness_pb2.pyi +366 -0
  31. evalsi-0.1.0/src/evalsi/harness/v1alpha1/harness_pb2_grpc.py +285 -0
  32. evalsi-0.1.0/src/evalsi/harness/v1alpha1/harness_pb2_grpc.pyi +115 -0
  33. evalsi-0.1.0/src/evalsi/judges/__init__.py +215 -0
  34. evalsi-0.1.0/src/evalsi/judges/anthropic.py +85 -0
  35. evalsi-0.1.0/src/evalsi/judges/cache.py +45 -0
  36. evalsi-0.1.0/src/evalsi/judges/openai_compatible.py +101 -0
  37. evalsi-0.1.0/src/evalsi/judges/structured.py +99 -0
  38. evalsi-0.1.0/src/evalsi/logging.py +323 -0
  39. evalsi-0.1.0/src/evalsi/mcp_server.py +731 -0
  40. evalsi-0.1.0/src/evalsi/packs/__init__.py +1 -0
  41. evalsi-0.1.0/src/evalsi/packs/agent.py +535 -0
  42. evalsi-0.1.0/src/evalsi/packs/code.py +118 -0
  43. evalsi-0.1.0/src/evalsi/packs/core.py +278 -0
  44. evalsi-0.1.0/src/evalsi/packs/finetune.py +365 -0
  45. evalsi-0.1.0/src/evalsi/packs/judge.py +143 -0
  46. evalsi-0.1.0/src/evalsi/packs/ml_classic.py +522 -0
  47. evalsi-0.1.0/src/evalsi/packs/ml_monitoring.py +467 -0
  48. evalsi-0.1.0/src/evalsi/packs/rag.py +234 -0
  49. evalsi-0.1.0/src/evalsi/packs/rl.py +741 -0
  50. evalsi-0.1.0/src/evalsi/packs/safety.py +162 -0
  51. evalsi-0.1.0/src/evalsi/packs/text.py +205 -0
  52. evalsi-0.1.0/src/evalsi/plugin/__init__.py +1 -0
  53. evalsi-0.1.0/src/evalsi/plugin/v1alpha1/__init__.py +1 -0
  54. evalsi-0.1.0/src/evalsi/plugin/v1alpha1/evaluator_plugin_pb2.py +70 -0
  55. evalsi-0.1.0/src/evalsi/plugin/v1alpha1/evaluator_plugin_pb2.pyi +358 -0
  56. evalsi-0.1.0/src/evalsi/plugin/v1alpha1/evaluator_plugin_pb2_grpc.py +346 -0
  57. evalsi-0.1.0/src/evalsi/plugin/v1alpha1/evaluator_plugin_pb2_grpc.pyi +160 -0
  58. evalsi-0.1.0/src/evalsi/plugins.py +258 -0
  59. evalsi-0.1.0/src/evalsi/py.typed +0 -0
  60. evalsi-0.1.0/src/evalsi/registry.py +150 -0
  61. evalsi-0.1.0/src/evalsi/report.css +21 -0
  62. evalsi-0.1.0/src/evalsi/report.py +346 -0
  63. evalsi-0.1.0/src/evalsi/results.py +331 -0
  64. evalsi-0.1.0/src/evalsi/rewards/__init__.py +597 -0
  65. evalsi-0.1.0/src/evalsi/rewards/openrlhf.py +114 -0
  66. evalsi-0.1.0/src/evalsi/rewards/spec.py +254 -0
  67. evalsi-0.1.0/src/evalsi/rewards/trl.py +76 -0
  68. evalsi-0.1.0/src/evalsi/rewards/verl.py +74 -0
  69. evalsi-0.1.0/src/evalsi/run.py +392 -0
  70. evalsi-0.1.0/src/evalsi/runner.py +284 -0
  71. evalsi-0.1.0/src/evalsi/runspec.py +226 -0
  72. evalsi-0.1.0/src/evalsi/sandbox/__init__.py +211 -0
  73. evalsi-0.1.0/src/evalsi/sandbox/client.py +411 -0
  74. evalsi-0.1.0/src/evalsi/sandbox/v1alpha1/__init__.py +1 -0
  75. evalsi-0.1.0/src/evalsi/sandbox/v1alpha1/sandbox_pb2.py +109 -0
  76. evalsi-0.1.0/src/evalsi/sandbox/v1alpha1/sandbox_pb2.pyi +772 -0
  77. evalsi-0.1.0/src/evalsi/sandbox/v1alpha1/sandbox_pb2_grpc.py +515 -0
  78. evalsi-0.1.0/src/evalsi/sandbox/v1alpha1/sandbox_pb2_grpc.pyi +175 -0
  79. evalsi-0.1.0/src/evalsi/stats.py +209 -0
  80. evalsi-0.1.0/src/evalsi/targets.py +267 -0
  81. evalsi-0.1.0/src/evalsi/testing.py +59 -0
  82. evalsi-0.1.0/src/evalsi/training/__init__.py +39 -0
  83. evalsi-0.1.0/src/evalsi/training/callback.py +205 -0
  84. evalsi-0.1.0/src/evalsi/training/checkpoints.py +197 -0
  85. evalsi-0.1.0/src/evalsi/training/loop.py +416 -0
  86. evalsi-0.1.0/src/evalsi/training/serving.py +152 -0
  87. evalsi-0.1.0/src/evalsi/types.py +453 -0
  88. evalsi-0.1.0/src/evalsi/v1alpha1/__init__.py +1 -0
  89. evalsi-0.1.0/src/evalsi/v1alpha1/agent_pb2.py +116 -0
  90. evalsi-0.1.0/src/evalsi/v1alpha1/agent_pb2.pyi +983 -0
  91. evalsi-0.1.0/src/evalsi/v1alpha1/annotation_service_pb2.py +112 -0
  92. evalsi-0.1.0/src/evalsi/v1alpha1/annotation_service_pb2.pyi +868 -0
  93. evalsi-0.1.0/src/evalsi/v1alpha1/annotation_service_pb2_grpc.py +457 -0
  94. evalsi-0.1.0/src/evalsi/v1alpha1/annotation_service_pb2_grpc.pyi +154 -0
  95. evalsi-0.1.0/src/evalsi/v1alpha1/auth_service_pb2.py +136 -0
  96. evalsi-0.1.0/src/evalsi/v1alpha1/auth_service_pb2.pyi +1004 -0
  97. evalsi-0.1.0/src/evalsi/v1alpha1/auth_service_pb2_grpc.py +719 -0
  98. evalsi-0.1.0/src/evalsi/v1alpha1/auth_service_pb2_grpc.pyi +220 -0
  99. evalsi-0.1.0/src/evalsi/v1alpha1/catalog_service_pb2.py +47 -0
  100. evalsi-0.1.0/src/evalsi/v1alpha1/catalog_service_pb2.pyi +132 -0
  101. evalsi-0.1.0/src/evalsi/v1alpha1/catalog_service_pb2_grpc.py +145 -0
  102. evalsi-0.1.0/src/evalsi/v1alpha1/catalog_service_pb2_grpc.pyi +73 -0
  103. evalsi-0.1.0/src/evalsi/v1alpha1/dataset_service_pb2.py +46 -0
  104. evalsi-0.1.0/src/evalsi/v1alpha1/dataset_service_pb2.pyi +130 -0
  105. evalsi-0.1.0/src/evalsi/v1alpha1/dataset_service_pb2_grpc.py +140 -0
  106. evalsi-0.1.0/src/evalsi/v1alpha1/dataset_service_pb2_grpc.pyi +58 -0
  107. evalsi-0.1.0/src/evalsi/v1alpha1/evaluation_service_pb2.py +55 -0
  108. evalsi-0.1.0/src/evalsi/v1alpha1/evaluation_service_pb2.pyi +219 -0
  109. evalsi-0.1.0/src/evalsi/v1alpha1/evaluation_service_pb2_grpc.py +149 -0
  110. evalsi-0.1.0/src/evalsi/v1alpha1/evaluation_service_pb2_grpc.pyi +82 -0
  111. evalsi-0.1.0/src/evalsi/v1alpha1/evaluator_pb2.py +50 -0
  112. evalsi-0.1.0/src/evalsi/v1alpha1/evaluator_pb2.pyi +271 -0
  113. evalsi-0.1.0/src/evalsi/v1alpha1/guardrail_service_pb2.py +89 -0
  114. evalsi-0.1.0/src/evalsi/v1alpha1/guardrail_service_pb2.pyi +534 -0
  115. evalsi-0.1.0/src/evalsi/v1alpha1/guardrail_service_pb2_grpc.py +282 -0
  116. evalsi-0.1.0/src/evalsi/v1alpha1/guardrail_service_pb2_grpc.pyi +106 -0
  117. evalsi-0.1.0/src/evalsi/v1alpha1/monitor_service_pb2.py +75 -0
  118. evalsi-0.1.0/src/evalsi/v1alpha1/monitor_service_pb2.pyi +458 -0
  119. evalsi-0.1.0/src/evalsi/v1alpha1/monitor_service_pb2_grpc.py +230 -0
  120. evalsi-0.1.0/src/evalsi/v1alpha1/monitor_service_pb2_grpc.pyi +85 -0
  121. evalsi-0.1.0/src/evalsi/v1alpha1/record_pb2.py +95 -0
  122. evalsi-0.1.0/src/evalsi/v1alpha1/record_pb2.pyi +727 -0
  123. evalsi-0.1.0/src/evalsi/v1alpha1/reward_service_pb2.py +60 -0
  124. evalsi-0.1.0/src/evalsi/v1alpha1/reward_service_pb2.pyi +311 -0
  125. evalsi-0.1.0/src/evalsi/v1alpha1/reward_service_pb2_grpc.py +112 -0
  126. evalsi-0.1.0/src/evalsi/v1alpha1/reward_service_pb2_grpc.pyi +70 -0
  127. evalsi-0.1.0/src/evalsi/v1alpha1/run_pb2.py +75 -0
  128. evalsi-0.1.0/src/evalsi/v1alpha1/run_pb2.pyi +582 -0
  129. evalsi-0.1.0/src/evalsi/v1alpha1/run_service_pb2.py +93 -0
  130. evalsi-0.1.0/src/evalsi/v1alpha1/run_service_pb2.pyi +593 -0
  131. evalsi-0.1.0/src/evalsi/v1alpha1/run_service_pb2_grpc.py +541 -0
  132. evalsi-0.1.0/src/evalsi/v1alpha1/run_service_pb2_grpc.pyi +181 -0
  133. evalsi-0.1.0/src/evalsi/v1alpha1/score_pb2.py +57 -0
  134. evalsi-0.1.0/src/evalsi/v1alpha1/score_pb2.pyi +305 -0
  135. evalsi-0.1.0/src/evalsi/v1alpha1/source_service_pb2.py +101 -0
  136. evalsi-0.1.0/src/evalsi/v1alpha1/source_service_pb2.pyi +742 -0
  137. evalsi-0.1.0/src/evalsi/v1alpha1/source_service_pb2_grpc.py +382 -0
  138. evalsi-0.1.0/src/evalsi/v1alpha1/source_service_pb2_grpc.pyi +154 -0
  139. evalsi-0.1.0/src/evalsi/v1alpha1/trace_service_pb2.py +62 -0
  140. evalsi-0.1.0/src/evalsi/v1alpha1/trace_service_pb2.pyi +315 -0
  141. evalsi-0.1.0/src/evalsi/v1alpha1/trace_service_pb2_grpc.py +192 -0
  142. evalsi-0.1.0/src/evalsi/v1alpha1/trace_service_pb2_grpc.pyi +91 -0
  143. evalsi-0.1.0/src/evalsi/v1alpha1/webhook_service_pb2.py +73 -0
  144. evalsi-0.1.0/src/evalsi/v1alpha1/webhook_service_pb2.pyi +458 -0
  145. evalsi-0.1.0/src/evalsi/v1alpha1/webhook_service_pb2_grpc.py +353 -0
  146. evalsi-0.1.0/src/evalsi/v1alpha1/webhook_service_pb2_grpc.pyi +157 -0
  147. evalsi-0.1.0/src/evalsi/wasm.py +256 -0
  148. evalsi-0.1.0/src/evalsi/webhooks.py +55 -0
  149. evalsi-0.1.0/src/evalsi/worker.py +335 -0
  150. evalsi-0.1.0/tests/conftest.py +96 -0
  151. evalsi-0.1.0/tests/test_analytics.py +210 -0
  152. evalsi-0.1.0/tests/test_annotate.py +238 -0
  153. evalsi-0.1.0/tests/test_auth.py +383 -0
  154. evalsi-0.1.0/tests/test_checkpoint_hook.py +75 -0
  155. evalsi-0.1.0/tests/test_cli.py +146 -0
  156. evalsi-0.1.0/tests/test_cli_server_summary.py +30 -0
  157. evalsi-0.1.0/tests/test_cli_sources.py +178 -0
  158. evalsi-0.1.0/tests/test_client_evaluate.py +233 -0
  159. evalsi-0.1.0/tests/test_code_pack.py +86 -0
  160. evalsi-0.1.0/tests/test_core_pack.py +123 -0
  161. evalsi-0.1.0/tests/test_datasets.py +116 -0
  162. evalsi-0.1.0/tests/test_demo.py +297 -0
  163. evalsi-0.1.0/tests/test_finetune_pack.py +139 -0
  164. evalsi-0.1.0/tests/test_guardrails.py +131 -0
  165. evalsi-0.1.0/tests/test_judge.py +315 -0
  166. evalsi-0.1.0/tests/test_logging.py +247 -0
  167. evalsi-0.1.0/tests/test_mcp.py +280 -0
  168. evalsi-0.1.0/tests/test_ml_packs.py +249 -0
  169. evalsi-0.1.0/tests/test_packs.py +407 -0
  170. evalsi-0.1.0/tests/test_plugins.py +146 -0
  171. evalsi-0.1.0/tests/test_registry.py +47 -0
  172. evalsi-0.1.0/tests/test_report.py +145 -0
  173. evalsi-0.1.0/tests/test_reward_trainers.py +136 -0
  174. evalsi-0.1.0/tests/test_rewards.py +272 -0
  175. evalsi-0.1.0/tests/test_rl_pack.py +244 -0
  176. evalsi-0.1.0/tests/test_runner.py +153 -0
  177. evalsi-0.1.0/tests/test_runs.py +417 -0
  178. evalsi-0.1.0/tests/test_sandbox_client.py +68 -0
  179. evalsi-0.1.0/tests/test_stats.py +100 -0
  180. evalsi-0.1.0/tests/test_training.py +510 -0
  181. evalsi-0.1.0/tests/test_types.py +78 -0
  182. evalsi-0.1.0/tests/test_webhooks.py +40 -0
  183. evalsi-0.1.0/tests/test_worker.py +386 -0
@@ -0,0 +1,30 @@
1
+ # Go
2
+ /evalsid
3
+ /bin/
4
+ # `go build ./cmd/...` without -o drops a binary in the working directory
5
+ /evalsi-guest
6
+ /evalsi-operator
7
+ /evalsi-collector
8
+
9
+ # Python
10
+ __pycache__/
11
+ *.py[cod]
12
+ .venv/
13
+ *.egg-info/
14
+ dist/
15
+ build/
16
+ .mypy_cache/
17
+ .pytest_cache/
18
+ .ruff_cache/
19
+
20
+ # Editors and OS
21
+ .idea/
22
+ .vscode/
23
+ .DS_Store
24
+
25
+ # Server data directories (examples use .evalsi and .evalsi-auth-demo)
26
+ .evalsi*/
27
+ examples/demo/terminal-bench-2/
28
+ examples/demo/_local-*.yaml
29
+
30
+ node_modules/
evalsi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.5
2
+ Name: evalsi
3
+ Version: 0.1.0
4
+ Summary: One entrypoint for evaluating ML models, LLMs and agents.
5
+ Project-URL: Homepage, https://abhishek-rnjn.github.io/Evals.si/
6
+ Project-URL: Documentation, https://abhishek-rnjn.github.io/Evals.si/
7
+ Project-URL: Repository, https://github.com/Abhishek-Rnjn/Evals.si
8
+ Project-URL: Issues, https://github.com/Abhishek-Rnjn/Evals.si/issues
9
+ Project-URL: Changelog, https://github.com/Abhishek-Rnjn/Evals.si/blob/main/CHANGELOG.md
10
+ License-Expression: Apache-2.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: protobuf>=7.35
18
+ Requires-Dist: pyyaml>=6.0
19
+ Provides-Extra: analytics
20
+ Requires-Dist: duckdb>=1.1; extra == 'analytics'
21
+ Provides-Extra: anthropic
22
+ Requires-Dist: anthropic>=1.0; extra == 'anthropic'
23
+ Provides-Extra: grpc
24
+ Requires-Dist: grpcio>=1.84; extra == 'grpc'
25
+ Provides-Extra: hf
26
+ Requires-Dist: datasets>=2.18; extra == 'hf'
27
+ Provides-Extra: jsonschema
28
+ Requires-Dist: jsonschema>=4.0; extra == 'jsonschema'
29
+ Provides-Extra: math
30
+ Requires-Dist: sympy>=1.13; extra == 'math'
31
+ Provides-Extra: server
32
+ Requires-Dist: grpcio-health-checking>=1.84; extra == 'server'
33
+ Requires-Dist: grpcio>=1.84; extra == 'server'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # evalsi
37
+
38
+ The Python SDK and CLI for [Evals.si](https://github.com/Abhishek-Rnjn/Evals.si): one entrypoint for evaluating ML models, LLMs and agents.
39
+
40
+ ```bash
41
+ pip install evalsi # core
42
+ pip install 'evalsi[anthropic]' # Claude as a judge
43
+ pip install 'evalsi[hf]' # hf:// datasets
44
+ ```
45
+
46
+ ```bash
47
+ evalsi eval --data qa.jsonl --evaluators exact-match,numeric-match
48
+ evalsi catalog
49
+ ```
50
+
51
+ ```python
52
+ import evalsi
53
+
54
+ result = evalsi.evaluate(
55
+ "qa.jsonl",
56
+ ["exact-match", "llm-judge"],
57
+ judge=evalsi.JudgeConfig(provider="anthropic", model="claude-opus-5-5"),
58
+ )
59
+ print(result.table())
60
+ ```
61
+
62
+ See the repository README for the full guide.
evalsi-0.1.0/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # evalsi
2
+
3
+ The Python SDK and CLI for [Evals.si](https://github.com/Abhishek-Rnjn/Evals.si): one entrypoint for evaluating ML models, LLMs and agents.
4
+
5
+ ```bash
6
+ pip install evalsi # core
7
+ pip install 'evalsi[anthropic]' # Claude as a judge
8
+ pip install 'evalsi[hf]' # hf:// datasets
9
+ ```
10
+
11
+ ```bash
12
+ evalsi eval --data qa.jsonl --evaluators exact-match,numeric-match
13
+ evalsi catalog
14
+ ```
15
+
16
+ ```python
17
+ import evalsi
18
+
19
+ result = evalsi.evaluate(
20
+ "qa.jsonl",
21
+ ["exact-match", "llm-judge"],
22
+ judge=evalsi.JudgeConfig(provider="anthropic", model="claude-opus-5-5"),
23
+ )
24
+ print(result.table())
25
+ ```
26
+
27
+ See the repository README for the full guide.
@@ -0,0 +1,66 @@
1
+ [project]
2
+ name = "evalsi"
3
+ dynamic = ["version"]
4
+ description = "One entrypoint for evaluating ML models, LLMs and agents."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "httpx>=0.27",
10
+ "protobuf>=7.35",
11
+ "pyyaml>=6.0",
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ anthropic = ["anthropic>=1.0"]
22
+ hf = ["datasets>=2.18"]
23
+ jsonschema = ["jsonschema>=4.0"]
24
+ # Symbolic answer equivalence in the rl pack's math-equiv.
25
+ math = ["sympy>=1.13"]
26
+ # Cross-run analytics (evalsi analyze).
27
+ analytics = ["duckdb>=1.1"]
28
+ # The worker that serves evaluators to the evalsid server over gRPC.
29
+ server = ["grpcio>=1.84", "grpcio-health-checking>=1.84"]
30
+ # The streaming client (Client.evaluate_stream).
31
+ grpc = ["grpcio>=1.84"]
32
+
33
+ [project.scripts]
34
+ evalsi = "evalsi.cli:main"
35
+
36
+ # Evaluator packs are discovered through this entry-point group, including the
37
+ # built-in ones, so third-party packs plug in exactly like ours.
38
+ [project.entry-points."evalsi.packs"]
39
+ core = "evalsi.packs.core:PACK"
40
+ judge = "evalsi.packs.judge:PACK"
41
+ text = "evalsi.packs.text:PACK"
42
+ rag = "evalsi.packs.rag:PACK"
43
+ safety = "evalsi.packs.safety:PACK"
44
+ agent = "evalsi.packs.agent:PACK"
45
+ code = "evalsi.packs.code:PACK"
46
+ rl = "evalsi.packs.rl:PACK"
47
+ finetune = "evalsi.packs.finetune:PACK"
48
+ ml-classic = "evalsi.packs.ml_classic:PACK"
49
+ ml-monitoring = "evalsi.packs.ml_monitoring:PACK"
50
+
51
+ [project.urls]
52
+ Homepage = "https://abhishek-rnjn.github.io/Evals.si/"
53
+ Documentation = "https://abhishek-rnjn.github.io/Evals.si/"
54
+ Repository = "https://github.com/Abhishek-Rnjn/Evals.si"
55
+ Issues = "https://github.com/Abhishek-Rnjn/Evals.si/issues"
56
+ Changelog = "https://github.com/Abhishek-Rnjn/Evals.si/blob/main/CHANGELOG.md"
57
+
58
+ [build-system]
59
+ requires = ["hatchling>=1.25"]
60
+ build-backend = "hatchling.build"
61
+
62
+ [tool.hatch.build.targets.wheel]
63
+ packages = ["src/evalsi"]
64
+
65
+ [tool.hatch.version]
66
+ path = "src/evalsi/_version.py"
@@ -0,0 +1,77 @@
1
+ """Evals.si: one entrypoint for evaluating ML models, LLMs and agents.
2
+
3
+ Quick start::
4
+
5
+ import evalsi
6
+
7
+ result = evalsi.evaluate("qa.jsonl", ["exact-match", "llm-judge"],
8
+ judge=evalsi.JudgeConfig(provider="anthropic",
9
+ model="claude-opus-5-5"))
10
+ print(result.table())
11
+ """
12
+
13
+ from evalsi._version import __version__
14
+ from evalsi.evaluator import (
15
+ EvalContext,
16
+ EvaluatorConfigError,
17
+ MetricSpec,
18
+ Requirements,
19
+ Scope,
20
+ ScoreType,
21
+ SkipRecord,
22
+ evaluator,
23
+ )
24
+ from evalsi.judges import JudgeConfig
25
+ from evalsi.logging import Logger, configure_logging, flush, log
26
+ from evalsi.registry import Pack, Registry
27
+ from evalsi.results import EvalResult, MetricSummary
28
+ from evalsi.runner import aevaluate, evaluate
29
+ from evalsi.types import (
30
+ Content,
31
+ EvaluationResult,
32
+ Message,
33
+ Outcome,
34
+ Record,
35
+ Score,
36
+ Step,
37
+ TaskCheck,
38
+ ToolCall,
39
+ ToolUse,
40
+ Trajectory,
41
+ Usage,
42
+ )
43
+
44
+ __all__ = [
45
+ "Content",
46
+ "EvalContext",
47
+ "EvalResult",
48
+ "EvaluationResult",
49
+ "EvaluatorConfigError",
50
+ "JudgeConfig",
51
+ "Logger",
52
+ "Message",
53
+ "MetricSpec",
54
+ "MetricSummary",
55
+ "Outcome",
56
+ "Pack",
57
+ "Record",
58
+ "Registry",
59
+ "Requirements",
60
+ "Scope",
61
+ "Score",
62
+ "ScoreType",
63
+ "SkipRecord",
64
+ "Step",
65
+ "TaskCheck",
66
+ "ToolCall",
67
+ "ToolUse",
68
+ "Trajectory",
69
+ "Usage",
70
+ "__version__",
71
+ "aevaluate",
72
+ "configure_logging",
73
+ "evaluate",
74
+ "evaluator",
75
+ "flush",
76
+ "log",
77
+ ]
@@ -0,0 +1,7 @@
1
+ """Allows `python -m evalsi`."""
2
+
3
+ import sys
4
+
5
+ from evalsi.cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,325 @@
1
+ """Cross-run analytics in DuckDB.
2
+
3
+ Runs, from results files (``evalsi run --output``) or from a server through
4
+ its API, are loaded into three tables:
5
+
6
+ ``runs``
7
+ run_id, name, project, status, model, source, created_at, labels (JSON)
8
+ ``scores``
9
+ run_id, record_id, trial, evaluator, metric, value, passed, label,
10
+ outcome, reason, explanation: one row per score, plus one per result
11
+ that was skipped or errored (value NULL)
12
+ ``records``
13
+ run_id, record_id, trial, input, output, reference, metadata (JSON):
14
+ one row per record, or per record and trial when a run produced the
15
+ outputs (trial -1 is a dataset record, the same in every trial)
16
+
17
+ so results can be sliced by record metadata across many runs, in SQL or
18
+ with :meth:`Warehouse.slice`. Going through the API, a server's results are
19
+ read the same way whatever its backend (SQLite, PostgreSQL, ClickHouse),
20
+ and only the runs the caller may read.
21
+
22
+ Needs ``duckdb`` (``pip install 'evalsi[analytics]'``).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import math
29
+ from collections.abc import Iterable, Mapping, Sequence
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ from evalsi.results import record_trial, result_metric
34
+
35
+ _SCHEMA = """
36
+ CREATE TABLE IF NOT EXISTS runs (
37
+ run_id VARCHAR PRIMARY KEY, name VARCHAR, project VARCHAR, status VARCHAR,
38
+ model VARCHAR, source VARCHAR, created_at TIMESTAMPTZ, labels JSON
39
+ );
40
+ CREATE TABLE IF NOT EXISTS scores (
41
+ run_id VARCHAR, record_id VARCHAR, trial INTEGER, evaluator VARCHAR,
42
+ metric VARCHAR, value DOUBLE, passed BOOLEAN, label VARCHAR,
43
+ outcome VARCHAR, reason VARCHAR, explanation VARCHAR
44
+ );
45
+ CREATE TABLE IF NOT EXISTS records (
46
+ run_id VARCHAR, record_id VARCHAR, trial INTEGER, input VARCHAR, output VARCHAR,
47
+ reference VARCHAR, metadata JSON
48
+ );
49
+ """
50
+ # Warehouse files from before records had a trial.
51
+ _MIGRATE = "ALTER TABLE records ADD COLUMN IF NOT EXISTS trial INTEGER DEFAULT -1"
52
+ _RECORD_COLUMNS = "run_id, record_id, trial, input, output, reference, metadata"
53
+
54
+
55
+ def _duckdb() -> Any:
56
+ try:
57
+ import duckdb
58
+ except ImportError as exc: # pragma: no cover - depends on the install
59
+ raise RuntimeError(
60
+ "cross-run analytics needs duckdb: pip install 'evalsi[analytics]'"
61
+ ) from exc
62
+ return duckdb
63
+
64
+
65
+ def _text(value: Any) -> str | None:
66
+ if value is None:
67
+ return None
68
+ if isinstance(value, Mapping):
69
+ if "text" in value:
70
+ return str(value["text"])
71
+ return json.dumps(value, ensure_ascii=False, default=str)
72
+ return str(value)
73
+
74
+
75
+ def _score_rows(run_id: str, results: Iterable[Mapping[str, Any]]) -> list[tuple[Any, ...]]:
76
+ rows: list[tuple[Any, ...]] = []
77
+ for r in results:
78
+ rid = str(r.get("record_id", r.get("recordId", "")))
79
+ trial = int(r.get("trial", 0) or 0)
80
+ evaluator = str(r.get("evaluator", ""))
81
+ ref = str(r.get("evaluator_ref", r.get("evaluatorRef", "")))
82
+ outcome = str(r.get("outcome", "")).lower().removeprefix("outcome_")
83
+ reason = str(r.get("reason", "")) or None
84
+ scores = r.get("scores") or []
85
+ if outcome != "scored" or not scores:
86
+ empty = (None, None, None, outcome, reason, None)
87
+ rows.append((run_id, rid, trial, evaluator, evaluator, *empty))
88
+ continue
89
+ for s in scores:
90
+ value: float | None = None
91
+ passed = s.get("passed")
92
+ if "number" in s and s["number"] is not None:
93
+ value = float(s["number"])
94
+ elif passed is not None:
95
+ value = 1.0 if passed else 0.0
96
+ label = s.get("label")
97
+ rows.append(
98
+ (
99
+ run_id,
100
+ rid,
101
+ trial,
102
+ evaluator,
103
+ result_metric(evaluator, ref, str(s.get("name", ""))),
104
+ value,
105
+ None if passed is None else bool(passed),
106
+ None if label is None else str(label),
107
+ outcome,
108
+ reason,
109
+ str(s.get("explanation", "")) or None,
110
+ )
111
+ )
112
+ return rows
113
+
114
+
115
+ def _record_rows(run_id: str, records: Iterable[Mapping[str, Any]]) -> list[tuple[Any, ...]]:
116
+ # One row per record and trial: result pages repeat a record for each
117
+ # page its results fall on.
118
+ rows: dict[tuple[str, int], tuple[Any, ...]] = {}
119
+ for r in records:
120
+ rid = str(r.get("id", ""))
121
+ trial = record_trial(r)
122
+ key = (rid, -1 if trial is None else trial)
123
+ rows[key] = (
124
+ run_id,
125
+ rid,
126
+ key[1],
127
+ _text(r.get("input")),
128
+ _text(r.get("output")),
129
+ _text(r.get("reference")),
130
+ json.dumps(r.get("metadata") or {}, ensure_ascii=False, default=str),
131
+ )
132
+ return list(rows.values())
133
+
134
+
135
+ class Warehouse:
136
+ """Runs loaded into DuckDB, in memory or in a file (``path``)."""
137
+
138
+ def __init__(self, path: str = ":memory:") -> None:
139
+ self.db = _duckdb().connect(path)
140
+ self.db.execute(_SCHEMA)
141
+ self.db.execute(_MIGRATE)
142
+
143
+ def close(self) -> None:
144
+ self.db.close()
145
+
146
+ def __enter__(self) -> Warehouse:
147
+ return self
148
+
149
+ def __exit__(self, *exc: object) -> None:
150
+ self.close()
151
+
152
+ def _replace(
153
+ self,
154
+ run: tuple[Any, ...],
155
+ results: Iterable[Mapping[str, Any]],
156
+ records: Iterable[Mapping[str, Any]],
157
+ ) -> None:
158
+ run_id = run[0]
159
+ self.db.execute("BEGIN")
160
+ try:
161
+ for table in ("runs", "scores", "records"):
162
+ self.db.execute(f"DELETE FROM {table} WHERE run_id = ?", [run_id])
163
+ self.db.execute("INSERT INTO runs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", list(run))
164
+ scores = _score_rows(run_id, results)
165
+ if scores:
166
+ self.db.executemany(
167
+ "INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", scores
168
+ )
169
+ recs = _record_rows(run_id, records)
170
+ if recs:
171
+ self.db.executemany(
172
+ f"INSERT INTO records ({_RECORD_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?)", recs
173
+ )
174
+ self.db.execute("COMMIT")
175
+ except Exception:
176
+ self.db.execute("ROLLBACK")
177
+ raise
178
+
179
+ def add_results(self, data: Mapping[str, Any], *, run_id: str = "") -> str:
180
+ """A results file's content (``evalsi run --output``). The run is
181
+ named by ``run_id``, else the manifest's run id or name and start."""
182
+ manifest = data.get("manifest") or {}
183
+ target = manifest.get("target") or {}
184
+ rid = (
185
+ run_id
186
+ or str(manifest.get("run_id") or "")
187
+ or f"{manifest.get('name') or 'run'}@{manifest.get('started_at') or ''}"
188
+ )
189
+ labels = (manifest.get("spec") or {}).get("labels") or manifest.get("labels") or {}
190
+ run = (
191
+ rid,
192
+ str(manifest.get("name") or ""),
193
+ str(manifest.get("project") or ""),
194
+ "finished",
195
+ str(target.get("model") or "") if isinstance(target, Mapping) else "",
196
+ "file",
197
+ manifest.get("started_at"),
198
+ json.dumps(labels, default=str),
199
+ )
200
+ self._replace(run, data.get("results") or [], data.get("records") or [])
201
+ return rid
202
+
203
+ def add_results_file(self, path: str | Path) -> str:
204
+ p = Path(path)
205
+ return self.add_results(json.loads(p.read_text(encoding="utf-8")), run_id="")
206
+
207
+ def add_server_run(self, client: Any, run: Mapping[str, Any]) -> str:
208
+ """A server run (as ``GetRun``/``ListRuns`` return it), with its results."""
209
+ run_id = str(run.get("id", ""))
210
+ results: list[dict[str, Any]] = []
211
+ records: list[dict[str, Any]] = []
212
+ token = ""
213
+ while True:
214
+ page = client.list_run_results(run_id, page_token=token)
215
+ results += page.get("results", [])
216
+ records += page.get("records", [])
217
+ token = page.get("nextPageToken", "")
218
+ if not token:
219
+ break
220
+ target = (run.get("spec") or {}).get("target") or {}
221
+ row = (
222
+ run_id,
223
+ str(run.get("name") or ""),
224
+ str(run.get("project") or ""),
225
+ str(run.get("status", "")).removeprefix("RUN_STATUS_").lower(),
226
+ str(target.get("model") or ""),
227
+ "server",
228
+ run.get("createdAt"),
229
+ json.dumps(run.get("labels") or {}),
230
+ )
231
+ self._replace(row, results, records)
232
+ return run_id
233
+
234
+ def add_server_runs(
235
+ self,
236
+ client: Any,
237
+ *,
238
+ project: str = "",
239
+ labels: Mapping[str, str] | None = None,
240
+ run_ids: Sequence[str] = (),
241
+ limit: int = 50,
242
+ ) -> list[str]:
243
+ """The newest ``limit`` runs of a project (with these labels), or the given runs."""
244
+ if run_ids:
245
+ return [self.add_server_run(client, client.get_run(r)) for r in run_ids]
246
+ loaded: list[str] = []
247
+ token = ""
248
+ while len(loaded) < limit:
249
+ page = client.list_runs(project=project, page_token=token)
250
+ for run in page.get("runs", []):
251
+ have = run.get("labels") or {}
252
+ if labels and any(have.get(k) != v for k, v in labels.items()):
253
+ continue
254
+ # ListRuns leaves the spec out (the target, the model); the
255
+ # run itself has it.
256
+ full = run if "spec" in run else client.get_run(str(run.get("id", "")))
257
+ loaded.append(self.add_server_run(client, full))
258
+ if len(loaded) >= limit:
259
+ break
260
+ token = page.get("nextPageToken", "")
261
+ if not token:
262
+ break
263
+ return loaded
264
+
265
+ def query(
266
+ self, sql: str, params: Sequence[Any] = ()
267
+ ) -> tuple[list[str], list[tuple[Any, ...]]]:
268
+ cur = self.db.execute(sql, list(params))
269
+ columns = [d[0] for d in cur.description or []]
270
+ return columns, cur.fetchall()
271
+
272
+ def slice(
273
+ self, metric: str, by: str, *, level: float = 0.95
274
+ ) -> tuple[list[str], list[tuple[Any, ...]]]:
275
+ """``metric`` per run and per value of the record metadata field
276
+ ``by`` (a dotted path), with a normal-approximation interval."""
277
+ z = {0.9: 1.6449, 0.95: 1.96, 0.99: 2.5758}.get(level)
278
+ if z is None:
279
+ raise ValueError("level must be 0.9, 0.95 or 0.99")
280
+ path = "$." + ".".join(json.dumps(p) for p in by.split("."))
281
+ alias = by.replace('"', '""')
282
+ _, rows = self.query(
283
+ f"""
284
+ SELECT s.run_id, any_value(r.name) AS run,
285
+ coalesce(
286
+ json_extract_string(coalesce(out.metadata, rec.metadata), ?), '(none)'
287
+ ) AS "{alias}",
288
+ count(s.value) AS n, avg(s.value) AS mean, stddev_samp(s.value) AS sd
289
+ FROM scores s
290
+ JOIN runs r USING (run_id)
291
+ -- The output of the score's own trial, else the dataset record:
292
+ -- at most one row each, so a score is counted once.
293
+ LEFT JOIN records out
294
+ ON out.run_id = s.run_id AND out.record_id = s.record_id AND out.trial = s.trial
295
+ LEFT JOIN records rec
296
+ ON rec.run_id = s.run_id AND rec.record_id = s.record_id AND rec.trial = -1
297
+ WHERE s.metric = ? AND s.value IS NOT NULL
298
+ GROUP BY ALL
299
+ ORDER BY 3, any_value(r.created_at), 1
300
+ """,
301
+ [path, metric],
302
+ )
303
+ out = []
304
+ for run_id, name, key, n, mean, sd in rows:
305
+ half = z * sd / math.sqrt(n) if n > 1 and sd is not None else None
306
+ low = None if half is None else mean - half
307
+ high = None if half is None else mean + half
308
+ out.append((run_id, name, key, n, mean, low, high))
309
+ return ["run_id", "run", by, "n", "mean", "low", "high"], out
310
+
311
+
312
+ def format_table(columns: Sequence[str], rows: Sequence[Sequence[Any]]) -> str:
313
+ def cell(v: Any) -> str:
314
+ if v is None:
315
+ return ""
316
+ if isinstance(v, float):
317
+ return f"{v:.3f}"
318
+ return str(v)
319
+
320
+ body = [[cell(v) for v in row] for row in rows]
321
+ widths = [max([len(c), *(len(r[i]) for r in body)]) for i, c in enumerate(columns)]
322
+ lines = [" ".join(c.ljust(w) for c, w in zip(columns, widths, strict=True)).rstrip()]
323
+ for r in body:
324
+ lines.append(" ".join(v.ljust(w) for v, w in zip(r, widths, strict=True)).rstrip())
325
+ return "\n".join(lines)