evalkit-py 0.2.0.dev12__py3-none-any.whl

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 (371) hide show
  1. evalkit/__init__.py +0 -0
  2. evalkit/api/__init__.py +0 -0
  3. evalkit/api/access.py +74 -0
  4. evalkit/api/agents.py +159 -0
  5. evalkit/api/app.py +360 -0
  6. evalkit/api/auth.py +225 -0
  7. evalkit/api/cli_dist.py +10 -0
  8. evalkit/api/datasets.py +169 -0
  9. evalkit/api/deps.py +79 -0
  10. evalkit/api/download.py +15 -0
  11. evalkit/api/evaluators.py +121 -0
  12. evalkit/api/experiments.py +616 -0
  13. evalkit/api/fixtures.py +95 -0
  14. evalkit/api/harness_check.py +48 -0
  15. evalkit/api/imports.py +180 -0
  16. evalkit/api/limits.py +84 -0
  17. evalkit/api/llm_providers.py +120 -0
  18. evalkit/api/mcp/__init__.py +0 -0
  19. evalkit/api/mcp/auth.py +100 -0
  20. evalkit/api/mcp/descriptions.py +336 -0
  21. evalkit/api/mcp/models.py +259 -0
  22. evalkit/api/mcp/server.py +101 -0
  23. evalkit/api/mcp/tools.py +844 -0
  24. evalkit/api/meta.py +19 -0
  25. evalkit/api/otel.py +350 -0
  26. evalkit/api/pull.py +197 -0
  27. evalkit/api/review.py +271 -0
  28. evalkit/api/runs.py +555 -0
  29. evalkit/api/server_keys.py +44 -0
  30. evalkit/api/tokens.py +51 -0
  31. evalkit/api/trace_imports.py +173 -0
  32. evalkit/api/trace_sources.py +49 -0
  33. evalkit/api/traces.py +196 -0
  34. evalkit/api/users.py +72 -0
  35. evalkit/api/workspaces.py +131 -0
  36. evalkit/bundled/contracts/atif-accepted.json +5 -0
  37. evalkit/bundled/contracts/custom-evaluator-api-v1.json +243 -0
  38. evalkit/bundled/contracts/harness-api-v1.json +938 -0
  39. evalkit/bundled/contracts/span-mapping-v1.json +314 -0
  40. evalkit/bundled/docs/concepts.md +26 -0
  41. evalkit/bundled/docs/config.md +38 -0
  42. evalkit/bundled/docs/harness-api.md +44 -0
  43. evalkit/bundled/docs/install.md +60 -0
  44. evalkit/bundled/docs/login.md +36 -0
  45. evalkit/bundled/docs/mappings.md +136 -0
  46. evalkit/bundled/docs/mcp.md +44 -0
  47. evalkit/bundled/docs/quickstart.md +142 -0
  48. evalkit/bundled/docs/sdk-runs.md +171 -0
  49. evalkit/bundled/docs/tracing.md +171 -0
  50. evalkit/bundled/docs/try-locally.md +288 -0
  51. evalkit/bundled/docs/workspaces.md +45 -0
  52. evalkit/bundled/skills/authoring-eval-cases/SKILL.md +387 -0
  53. evalkit/bundled/skills/authoring-eval-cases/references/overlays.md +83 -0
  54. evalkit/bundled/skills/authoring-eval-cases/references/tool-checks.md +71 -0
  55. evalkit/bundled/skills/connect-traces/SKILL.md +296 -0
  56. evalkit/bundled/skills/connect-traces/references/gen_ai.json +42 -0
  57. evalkit/bundled/skills/connect-traces/references/openinference.json +35 -0
  58. evalkit/bundled/skills/connect-traces/references/span-mapping-schema.md +88 -0
  59. evalkit/bundled/skills/eval-driven-development/SKILL.md +458 -0
  60. evalkit/bundled/skills/eval-driven-development/references/analyze-run.md +97 -0
  61. evalkit/bundled/skills/eval-driven-development/references/diagnose.md +130 -0
  62. evalkit/bundled/skills/eval-driven-development/references/synthesize-cases.md +87 -0
  63. evalkit/bundled/skills/eval-driven-development/references/synthesize.md +165 -0
  64. evalkit/bundled/skills/evalkit-deployment/SKILL.md +332 -0
  65. evalkit/bundled/skills/evalkit-deployment/config.example.yaml +49 -0
  66. evalkit/bundled/skills/evalkit-deployment/docker-compose.yml +51 -0
  67. evalkit/bundled/skills/evalkit-deployment/references/config-and-deploy.md +333 -0
  68. evalkit/bundled/skills/evalkit-deployment/references/harness-api.md +139 -0
  69. evalkit/bundled/skills/evalkit-deployment/try/.env.example +11 -0
  70. evalkit/bundled/skills/evalkit-deployment/try/agent.py +131 -0
  71. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/balance-sheet/cases.yaml +25 -0
  72. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/income-statement/cases.yaml +43 -0
  73. evalkit/bundled/skills/evalkit-deployment/try/config.yaml +17 -0
  74. evalkit/bundled/skills/evalkit-deployment/try/docker-compose.yml +82 -0
  75. evalkit/bundled/skills/evalkit-deployment/try/evalkit.yaml +4 -0
  76. evalkit/bundled/skills/evalkit-deployment/try/mapping.json +42 -0
  77. evalkit/bundled/skills/evalkit-deployment/try/run.py +33 -0
  78. evalkit/bundled/skills/evalkit-usage/SKILL.md +509 -0
  79. evalkit/bundled/skills/evalkit-usage/references/suite-fixtures.md +42 -0
  80. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  81. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  82. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  83. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  84. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  85. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  86. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  87. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  88. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  89. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  90. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  91. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  92. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  93. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  94. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  95. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  96. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  97. evalkit/bundled/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  98. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  99. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  100. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  101. evalkit/bundled/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  102. evalkit/bundled/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  103. evalkit/bundled/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  104. evalkit/bundled/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  105. evalkit/bundled/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  106. evalkit/bundled/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  107. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  108. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  109. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  110. evalkit/bundled/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  111. evalkit/bundled/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  112. evalkit/bundled/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  113. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  114. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  115. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  116. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  117. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  118. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  119. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  120. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  121. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  122. evalkit/bundled/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  123. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  124. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  125. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  126. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  127. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  128. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  129. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  130. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  131. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  132. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  133. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  134. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  135. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  136. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  137. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  138. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  139. evalkit/bundled/ui/assets/abap-08VXUWAP.js +1 -0
  140. evalkit/bundled/ui/assets/apex-BWPQTe0t.js +1 -0
  141. evalkit/bundled/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  142. evalkit/bundled/ui/assets/bat-i0X4ZdIN.js +1 -0
  143. evalkit/bundled/ui/assets/bicep-B5-_aFwp.js +2 -0
  144. evalkit/bundled/ui/assets/cameligo-DMUM7wLl.js +1 -0
  145. evalkit/bundled/ui/assets/clojure-Cm7r79vr.js +1 -0
  146. evalkit/bundled/ui/assets/codicon-DF1abBS2.ttf +0 -0
  147. evalkit/bundled/ui/assets/coffee-Ba7i2nA0.js +1 -0
  148. evalkit/bundled/ui/assets/cpp-C7h46wYY.js +1 -0
  149. evalkit/bundled/ui/assets/csharp-BKxtCVv1.js +1 -0
  150. evalkit/bundled/ui/assets/csp-bTuwJoIa.js +1 -0
  151. evalkit/bundled/ui/assets/css-DIMkf-bt.js +3 -0
  152. evalkit/bundled/ui/assets/css.worker-DwfUZ3fB.js +96 -0
  153. evalkit/bundled/ui/assets/cssMode-C97l1pbB.js +1 -0
  154. evalkit/bundled/ui/assets/cypher-CVaqCwHa.js +1 -0
  155. evalkit/bundled/ui/assets/dart-onAF5SnQ.js +1 -0
  156. evalkit/bundled/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  157. evalkit/bundled/ui/assets/ecl-94qehhXe.js +1 -0
  158. evalkit/bundled/ui/assets/editor-DjpsE8to.css +1 -0
  159. evalkit/bundled/ui/assets/editor.api-NTesIxw5.js +908 -0
  160. evalkit/bundled/ui/assets/editor.worker-CRz__AKW.js +33 -0
  161. evalkit/bundled/ui/assets/elixir-6RTg0lbw.js +1 -0
  162. evalkit/bundled/ui/assets/flow9-C5_-GSwl.js +1 -0
  163. evalkit/bundled/ui/assets/freemarker2-BWqEFagp.js +3 -0
  164. evalkit/bundled/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  165. evalkit/bundled/ui/assets/go-C-y9NEjX.js +1 -0
  166. evalkit/bundled/ui/assets/graphql-fmXr3nnJ.js +1 -0
  167. evalkit/bundled/ui/assets/handlebars-DkIF-whN.js +1 -0
  168. evalkit/bundled/ui/assets/hcl-CpzslTdj.js +1 -0
  169. evalkit/bundled/ui/assets/html-C_rpeQXJ.js +1 -0
  170. evalkit/bundled/ui/assets/html.worker-D9ugpX4y.js +509 -0
  171. evalkit/bundled/ui/assets/htmlMode-B0YIq3DF.js +1 -0
  172. evalkit/bundled/ui/assets/index-Cw-jBH6M.js +736 -0
  173. evalkit/bundled/ui/assets/index-DKtoQZtm.css +1 -0
  174. evalkit/bundled/ui/assets/ini-sBoK_t0W.js +1 -0
  175. evalkit/bundled/ui/assets/java-BEtHBSE6.js +1 -0
  176. evalkit/bundled/ui/assets/javascript-D4EGhYA6.js +1 -0
  177. evalkit/bundled/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  178. evalkit/bundled/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  179. evalkit/bundled/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  180. evalkit/bundled/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  181. evalkit/bundled/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  182. evalkit/bundled/ui/assets/json.worker-CV9fqkUp.js +65 -0
  183. evalkit/bundled/ui/assets/jsonMode-pQjSfIH6.js +7 -0
  184. evalkit/bundled/ui/assets/julia-Bri6UV-V.js +1 -0
  185. evalkit/bundled/ui/assets/kotlin-BOotOW0E.js +1 -0
  186. evalkit/bundled/ui/assets/less-B9JPFI3C.js +2 -0
  187. evalkit/bundled/ui/assets/lexon-CfSJPG6W.js +1 -0
  188. evalkit/bundled/ui/assets/liquid-DbrvoYjD.js +1 -0
  189. evalkit/bundled/ui/assets/lspLanguageFeatures-D0B7cH6y.js +4 -0
  190. evalkit/bundled/ui/assets/lua-CsQS60Ue.js +1 -0
  191. evalkit/bundled/ui/assets/m3-D-oSqn_W.js +1 -0
  192. evalkit/bundled/ui/assets/markdown-Cimd5fb3.js +1 -0
  193. evalkit/bundled/ui/assets/mdx-B0HrujLR.js +1 -0
  194. evalkit/bundled/ui/assets/mips-CIPQ_RoX.js +1 -0
  195. evalkit/bundled/ui/assets/monacoEditor-DsoPncxW.js +68 -0
  196. evalkit/bundled/ui/assets/monacoEditor-n7clI0si.css +1 -0
  197. evalkit/bundled/ui/assets/msdax-DauUninz.js +1 -0
  198. evalkit/bundled/ui/assets/mysql-SOo6toE5.js +1 -0
  199. evalkit/bundled/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  200. evalkit/bundled/ui/assets/pascal-DrH0SRf2.js +1 -0
  201. evalkit/bundled/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  202. evalkit/bundled/ui/assets/perl-oz_6vUea.js +1 -0
  203. evalkit/bundled/ui/assets/pgsql-DTj74zXo.js +1 -0
  204. evalkit/bundled/ui/assets/php-nr791fC2.js +1 -0
  205. evalkit/bundled/ui/assets/pla-CopQ2nXW.js +1 -0
  206. evalkit/bundled/ui/assets/postiats-43DmfD33.js +1 -0
  207. evalkit/bundled/ui/assets/powerquery-D3hlyOfw.js +1 -0
  208. evalkit/bundled/ui/assets/powershell-DmHpPYUd.js +1 -0
  209. evalkit/bundled/ui/assets/protobuf-C531GsRP.js +2 -0
  210. evalkit/bundled/ui/assets/pug-Z5eAx3Zn.js +1 -0
  211. evalkit/bundled/ui/assets/python-CieHVM9n.js +1 -0
  212. evalkit/bundled/ui/assets/qsharp-DkqhCAOL.js +1 -0
  213. evalkit/bundled/ui/assets/r-BwWrilGY.js +1 -0
  214. evalkit/bundled/ui/assets/razor-sLewSMyp.js +1 -0
  215. evalkit/bundled/ui/assets/redis-ClamHrr6.js +1 -0
  216. evalkit/bundled/ui/assets/redshift-DT7zqm-g.js +1 -0
  217. evalkit/bundled/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  218. evalkit/bundled/ui/assets/ruby-DezsRK8O.js +1 -0
  219. evalkit/bundled/ui/assets/rust-DdL9SqIa.js +1 -0
  220. evalkit/bundled/ui/assets/sb-CcwsVR0C.js +1 -0
  221. evalkit/bundled/ui/assets/scala-DHpiXF5c.js +1 -0
  222. evalkit/bundled/ui/assets/scheme-BeGwcela.js +1 -0
  223. evalkit/bundled/ui/assets/scss-gp-XZpBa.js +3 -0
  224. evalkit/bundled/ui/assets/shell-CC2rA5mh.js +1 -0
  225. evalkit/bundled/ui/assets/solidity-BEEn4gHE.js +1 -0
  226. evalkit/bundled/ui/assets/sophia-CRfGWb83.js +1 -0
  227. evalkit/bundled/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  228. evalkit/bundled/ui/assets/sql-NEE52Syq.js +1 -0
  229. evalkit/bundled/ui/assets/st-DbInun42.js +1 -0
  230. evalkit/bundled/ui/assets/swift-Bxkupp3x.js +1 -0
  231. evalkit/bundled/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  232. evalkit/bundled/ui/assets/tcl-DISqw1ZD.js +1 -0
  233. evalkit/bundled/ui/assets/ts.worker-DIAQkoB3.js +67741 -0
  234. evalkit/bundled/ui/assets/tsMode-BcVQ1nu0.js +11 -0
  235. evalkit/bundled/ui/assets/twig-De2hgUGE.js +1 -0
  236. evalkit/bundled/ui/assets/typescript-CUvNWd9p.js +1 -0
  237. evalkit/bundled/ui/assets/typespec-B8J7ngcE.js +1 -0
  238. evalkit/bundled/ui/assets/vb-DV3o63ZY.js +1 -0
  239. evalkit/bundled/ui/assets/wgsl-DpFanUEy.js +298 -0
  240. evalkit/bundled/ui/assets/workers-CwpYQtzp.js +1 -0
  241. evalkit/bundled/ui/assets/xml-C1HnXMdF.js +1 -0
  242. evalkit/bundled/ui/assets/yaml-D7Ey3HQu.js +1 -0
  243. evalkit/bundled/ui/favicon.svg +6 -0
  244. evalkit/bundled/ui/index.html +14 -0
  245. evalkit/cli/__init__.py +0 -0
  246. evalkit/cli/admin_commands.py +590 -0
  247. evalkit/cli/client.py +67 -0
  248. evalkit/cli/init_server.py +165 -0
  249. evalkit/cli/local_run.py +195 -0
  250. evalkit/cli/main.py +1365 -0
  251. evalkit/cli/runs_commands.py +539 -0
  252. evalkit/cli/server_commands.py +130 -0
  253. evalkit/cli/skill_targets.py +43 -0
  254. evalkit/cli/tokens_commands.py +52 -0
  255. evalkit/cli/traces_commands.py +332 -0
  256. evalkit/cli/update.py +85 -0
  257. evalkit/client/__init__.py +0 -0
  258. evalkit/client/agents.py +25 -0
  259. evalkit/client/api.py +193 -0
  260. evalkit/client/blob.py +104 -0
  261. evalkit/client/connection.py +88 -0
  262. evalkit/client/evaluators.py +11 -0
  263. evalkit/client/sessions.py +17 -0
  264. evalkit/client/store.py +139 -0
  265. evalkit/client/summary.py +46 -0
  266. evalkit/client/sync.py +198 -0
  267. evalkit/config.py +227 -0
  268. evalkit/contracts/__init__.py +0 -0
  269. evalkit/contracts/atif.py +115 -0
  270. evalkit/contracts/cases.py +456 -0
  271. evalkit/contracts/custom_evaluator.py +58 -0
  272. evalkit/contracts/enums.py +86 -0
  273. evalkit/contracts/esf.py +120 -0
  274. evalkit/contracts/harness.py +281 -0
  275. evalkit/contracts/notes.json +4 -0
  276. evalkit/contracts/notes.py +7 -0
  277. evalkit/contracts/settings.py +41 -0
  278. evalkit/core/__init__.py +0 -0
  279. evalkit/core/access.py +75 -0
  280. evalkit/core/agents.py +65 -0
  281. evalkit/core/blob.py +46 -0
  282. evalkit/core/capabilities.py +36 -0
  283. evalkit/core/costmap.py +90 -0
  284. evalkit/core/custom_eval.py +50 -0
  285. evalkit/core/data/cost_map.json +81546 -0
  286. evalkit/core/endpoints.py +31 -0
  287. evalkit/core/evaluators/__init__.py +29 -0
  288. evalkit/core/evaluators/base.py +121 -0
  289. evalkit/core/evaluators/gauges.py +84 -0
  290. evalkit/core/evaluators/presentation.py +166 -0
  291. evalkit/core/evaluators/response_time.py +21 -0
  292. evalkit/core/evaluators/rubrics.py +494 -0
  293. evalkit/core/evaluators/tool_trajectory.py +187 -0
  294. evalkit/core/evaluators/trajectory_rubrics.py +198 -0
  295. evalkit/core/fixtures.py +57 -0
  296. evalkit/core/harness.py +227 -0
  297. evalkit/core/harness_check.py +191 -0
  298. evalkit/core/importer.py +257 -0
  299. evalkit/core/judge.py +78 -0
  300. evalkit/core/lint.py +265 -0
  301. evalkit/core/overlays.py +76 -0
  302. evalkit/core/paths.py +25 -0
  303. evalkit/core/promotion.py +94 -0
  304. evalkit/core/pull.py +198 -0
  305. evalkit/core/python_eval.py +59 -0
  306. evalkit/core/remote_evaluators.py +32 -0
  307. evalkit/core/review.py +220 -0
  308. evalkit/core/run_digest.py +421 -0
  309. evalkit/core/runner.py +995 -0
  310. evalkit/core/scorecard.py +255 -0
  311. evalkit/core/traces/__init__.py +0 -0
  312. evalkit/core/traces/convert.py +336 -0
  313. evalkit/core/traces/importer.py +113 -0
  314. evalkit/core/traces/mapping.py +385 -0
  315. evalkit/core/traces/poll.py +38 -0
  316. evalkit/core/traces/raw.py +21 -0
  317. evalkit/core/traces/sessions.py +172 -0
  318. evalkit/core/traces/settle.py +14 -0
  319. evalkit/core/traces/sources/__init__.py +19 -0
  320. evalkit/core/traces/sources/base.py +25 -0
  321. evalkit/core/traces/sources/phoenix.py +121 -0
  322. evalkit/core/trajectory.py +248 -0
  323. evalkit/core/workspace.py +30 -0
  324. evalkit/credentials.py +119 -0
  325. evalkit/db/__init__.py +0 -0
  326. evalkit/db/agents.py +289 -0
  327. evalkit/db/alembic/env.py +90 -0
  328. evalkit/db/alembic/script.py.mako +28 -0
  329. evalkit/db/alembic/versions/0011_baseline.py +1002 -0
  330. evalkit/db/alembic/versions/0012_ticket_token_kind.py +27 -0
  331. evalkit/db/alembic/versions/0013_keys_and_llm_providers.py +67 -0
  332. evalkit/db/cases.py +608 -0
  333. evalkit/db/crypto.py +12 -0
  334. evalkit/db/evaluators.py +156 -0
  335. evalkit/db/experiments.py +363 -0
  336. evalkit/db/fixture_blobs.py +91 -0
  337. evalkit/db/keys.py +78 -0
  338. evalkit/db/llm_providers.py +176 -0
  339. evalkit/db/locks.py +6 -0
  340. evalkit/db/mappings.py +168 -0
  341. evalkit/db/models.py +567 -0
  342. evalkit/db/promotion.py +271 -0
  343. evalkit/db/rekey.py +46 -0
  344. evalkit/db/review.py +250 -0
  345. evalkit/db/review_state.py +50 -0
  346. evalkit/db/runs.py +533 -0
  347. evalkit/db/scorecard.py +300 -0
  348. evalkit/db/session.py +71 -0
  349. evalkit/db/sessions.py +90 -0
  350. evalkit/db/spans.py +360 -0
  351. evalkit/db/store.py +209 -0
  352. evalkit/db/tokens.py +100 -0
  353. evalkit/db/trace_imports.py +308 -0
  354. evalkit/db/trace_sources.py +131 -0
  355. evalkit/db/users.py +186 -0
  356. evalkit/db/workspaces.py +232 -0
  357. evalkit/paths.py +33 -0
  358. evalkit/sdk/__init__.py +29 -0
  359. evalkit/sdk/binding.py +69 -0
  360. evalkit/sdk/evaluators.py +56 -0
  361. evalkit/sdk/runner.py +957 -0
  362. evalkit/sdk/tracing.py +166 -0
  363. evalkit/store/__init__.py +0 -0
  364. evalkit/store/base.py +354 -0
  365. evalkit/store/blob.py +69 -0
  366. evalkit/version.py +44 -0
  367. evalkit_py-0.2.0.dev12.dist-info/METADATA +205 -0
  368. evalkit_py-0.2.0.dev12.dist-info/RECORD +371 -0
  369. evalkit_py-0.2.0.dev12.dist-info/WHEEL +4 -0
  370. evalkit_py-0.2.0.dev12.dist-info/entry_points.txt +2 -0
  371. evalkit_py-0.2.0.dev12.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,27 @@
1
+ """ticket token kind
2
+
3
+ Revision ID: 0012
4
+ Revises: 0011
5
+ Create Date: 2026-10-25
6
+
7
+ """
8
+
9
+ from alembic import op
10
+
11
+ revision = "0012"
12
+ down_revision = "0011"
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ op.drop_constraint("ck_auth_tokens_kind", "auth_tokens", type_="check")
19
+ op.create_check_constraint(
20
+ "ck_auth_tokens_kind", "auth_tokens", "kind IN ('user', 'agent', 'ticket')"
21
+ )
22
+
23
+
24
+ def downgrade() -> None:
25
+ op.execute("DELETE FROM auth_tokens WHERE kind = 'ticket'")
26
+ op.drop_constraint("ck_auth_tokens_kind", "auth_tokens", type_="check")
27
+ op.create_check_constraint("ck_auth_tokens_kind", "auth_tokens", "kind IN ('user', 'agent')")
@@ -0,0 +1,67 @@
1
+ """keys and llm providers
2
+
3
+ Revision ID: 0013
4
+ Revises: 0012
5
+ Create Date: 2026-10-29
6
+
7
+ """
8
+
9
+ import sqlalchemy as sa
10
+ from alembic import op
11
+ from sqlalchemy.dialects import postgresql
12
+
13
+ revision = "0013"
14
+ down_revision = "0012"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def _audit() -> list[sa.Column]:
20
+ return [
21
+ sa.Column(
22
+ "created_at",
23
+ sa.DateTime(timezone=True),
24
+ server_default=sa.text("now()"),
25
+ nullable=False,
26
+ ),
27
+ sa.Column(
28
+ "updated_at",
29
+ sa.DateTime(timezone=True),
30
+ server_default=sa.text("now()"),
31
+ nullable=False,
32
+ ),
33
+ ]
34
+
35
+
36
+ def upgrade() -> None:
37
+ op.add_column("agents", sa.Column("api_key_encrypted", sa.LargeBinary(), nullable=True))
38
+ op.add_column(
39
+ "remote_evaluators", sa.Column("api_key_encrypted", sa.LargeBinary(), nullable=True)
40
+ )
41
+ op.create_table(
42
+ "llm_providers",
43
+ sa.Column("id", sa.Integer(), nullable=False),
44
+ sa.Column("name", sa.String(length=150), nullable=False),
45
+ sa.Column("base_url", sa.String(length=500), nullable=False),
46
+ sa.Column("api_key_encrypted", sa.LargeBinary(), nullable=True),
47
+ sa.Column("archived_at", sa.DateTime(timezone=True), nullable=True),
48
+ *_audit(),
49
+ sa.PrimaryKeyConstraint("id"),
50
+ sa.UniqueConstraint("name", name="uq_llm_providers_name"),
51
+ sa.UniqueConstraint("base_url", name="uq_llm_providers_base_url"),
52
+ )
53
+ op.create_table(
54
+ "server_settings",
55
+ sa.Column("id", sa.Integer(), nullable=False),
56
+ sa.Column("default_judge", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
57
+ *_audit(),
58
+ sa.CheckConstraint("id = 1", name="ck_server_settings_single_row"),
59
+ sa.PrimaryKeyConstraint("id"),
60
+ )
61
+
62
+
63
+ def downgrade() -> None:
64
+ op.drop_table("server_settings")
65
+ op.drop_table("llm_providers")
66
+ op.drop_column("remote_evaluators", "api_key_encrypted")
67
+ op.drop_column("agents", "api_key_encrypted")
evalkit/db/cases.py ADDED
@@ -0,0 +1,608 @@
1
+ from datetime import UTC, datetime
2
+ from typing import Literal
3
+
4
+ from sqlalchemy import delete, func, select, true, tuple_, update
5
+ from sqlalchemy.dialects.postgresql import insert
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from evalkit.contracts.enums import DatasetKind, SyncSource
9
+ from evalkit.core.access import WorkspaceRef
10
+ from evalkit.core.overlays import OverlayBlock, apply_overlay_blocks
11
+ from evalkit.core.review import CaseEventKind, CaseState
12
+ from evalkit.core.workspace import Reference, parse_slug
13
+ from evalkit.db import models
14
+ from evalkit.db.review_state import latest_events
15
+ from evalkit.db.users import check_name
16
+ from evalkit.store.base import (
17
+ CarriedReview,
18
+ CaseFile,
19
+ CaseView,
20
+ DatasetRow,
21
+ DatasetSyncView,
22
+ IncomingCase,
23
+ IncomingOverlay,
24
+ SyncResult,
25
+ UnknownDataset,
26
+ )
27
+
28
+ Change = Literal["created", "patched", "unchanged"]
29
+
30
+
31
+ def _sync_view(row: models.DatasetSync, actor: str) -> DatasetSyncView:
32
+ return DatasetSyncView(
33
+ id=row.id,
34
+ source=row.source,
35
+ detail=row.detail,
36
+ actor=row.detail.get("actor") or actor,
37
+ created_at=row.created_at,
38
+ )
39
+
40
+
41
+ async def _last_syncs(session: AsyncSession, dataset_ids: list[int]) -> dict[int, DatasetSyncView]:
42
+ if not dataset_ids:
43
+ return {}
44
+ rows = (
45
+ await session.execute(
46
+ select(models.DatasetSync, models.User.username)
47
+ .join(models.User, models.User.id == models.DatasetSync.actor_id)
48
+ .where(models.DatasetSync.dataset_id.in_(dataset_ids))
49
+ .distinct(models.DatasetSync.dataset_id)
50
+ .order_by(models.DatasetSync.dataset_id, models.DatasetSync.id.desc())
51
+ )
52
+ ).all()
53
+ return {row.dataset_id: _sync_view(row, actor) for row, actor in rows}
54
+
55
+
56
+ def _dataset_query():
57
+ return select(models.Dataset, models.Workspace).join(
58
+ models.Workspace, models.Workspace.id == models.Dataset.workspace_id
59
+ )
60
+
61
+
62
+ async def _rows(session: AsyncSession, query) -> list[DatasetRow]:
63
+ found = (await session.execute(query)).all()
64
+ syncs = await _last_syncs(session, [d.id for d, _ in found])
65
+ return [
66
+ DatasetRow(
67
+ id=d.id,
68
+ workspace=w.name,
69
+ workspace_is_shared=w.owner_user_id is None,
70
+ name=d.name,
71
+ kind=d.kind,
72
+ last_sync=syncs.get(d.id),
73
+ )
74
+ for d, w in found
75
+ ]
76
+
77
+
78
+ async def get_dataset(session: AsyncSession, workspace: str, name: str) -> DatasetRow:
79
+ rows = await _rows(
80
+ session,
81
+ _dataset_query().where(models.Workspace.name == workspace, models.Dataset.name == name),
82
+ )
83
+ if not rows:
84
+ raise UnknownDataset(f"{name}@{workspace}")
85
+ return rows[0]
86
+
87
+
88
+ async def dataset_by_ref(session: AsyncSession, ref: str) -> DatasetRow:
89
+ parsed = parse_slug(ref)
90
+ return await get_dataset(session, parsed.workspace, parsed.name)
91
+
92
+
93
+ async def find_dataset_row(session: AsyncSession, workspace: str, name: str) -> DatasetRow | None:
94
+ rows = await _rows(
95
+ session,
96
+ _dataset_query().where(models.Workspace.name == workspace, models.Dataset.name == name),
97
+ )
98
+ return rows[0] if rows else None
99
+
100
+
101
+ async def list_datasets(session: AsyncSession, workspace: str | None = None) -> list[DatasetRow]:
102
+ query = _dataset_query().where(models.Dataset.archived_at.is_(None))
103
+ if workspace is not None:
104
+ query = query.where(models.Workspace.name == workspace)
105
+ return await _rows(
106
+ session,
107
+ query.order_by(
108
+ models.Workspace.owner_user_id.is_not(None), models.Workspace.name, models.Dataset.name
109
+ ),
110
+ )
111
+
112
+
113
+ async def case_ref_for(session: AsyncSession, case_id: str) -> int:
114
+ await session.execute(
115
+ insert(models.Case)
116
+ .values(case_id=case_id)
117
+ .on_conflict_do_nothing(index_elements=["case_id"])
118
+ )
119
+ found = await session.scalar(select(models.Case.id).where(models.Case.case_id == case_id))
120
+ assert found is not None
121
+ return found
122
+
123
+
124
+ async def case_refs_for(session: AsyncSession, case_ids: list[str]) -> dict[str, int]:
125
+ if not case_ids:
126
+ return {}
127
+ unique = list(dict.fromkeys(case_ids))
128
+ await session.execute(
129
+ insert(models.Case)
130
+ .values([{"case_id": case_id} for case_id in unique])
131
+ .on_conflict_do_nothing(index_elements=["case_id"])
132
+ )
133
+ rows = await session.execute(
134
+ select(models.Case.case_id, models.Case.id).where(models.Case.case_id.in_(unique))
135
+ )
136
+ return {case_id: ref for case_id, ref in rows.all()}
137
+
138
+
139
+ async def store_versions(session: AsyncSession, versions: list[tuple[int, IncomingCase]]) -> None:
140
+ if not versions:
141
+ return
142
+ await session.execute(
143
+ insert(models.CaseVersion)
144
+ .values(
145
+ [
146
+ {
147
+ "case_ref": case_ref,
148
+ "content_hash": case.content_hash,
149
+ "input": case.input,
150
+ "expected": case.expected,
151
+ "evaluators": case.evaluators,
152
+ "tags": case.tags,
153
+ }
154
+ for case_ref, case in versions
155
+ ]
156
+ )
157
+ .on_conflict_do_nothing(index_elements=["case_ref", "content_hash"])
158
+ )
159
+ files = [
160
+ {
161
+ "case_ref": case_ref,
162
+ "content_hash": case.content_hash,
163
+ "key": file.key,
164
+ "sha256": file.sha256,
165
+ "context": file.context,
166
+ "position": position,
167
+ }
168
+ for case_ref, case in versions
169
+ for position, file in enumerate(case.files)
170
+ ]
171
+ if files:
172
+ await session.execute(
173
+ insert(models.CaseVersionFile)
174
+ .values(files)
175
+ .on_conflict_do_nothing(index_elements=["case_ref", "content_hash", "key"])
176
+ )
177
+
178
+
179
+ async def store_version(session: AsyncSession, case_ref: int, case: IncomingCase) -> None:
180
+ await store_versions(session, [(case_ref, case)])
181
+
182
+
183
+ async def _carry_or_reset(
184
+ session: AsyncSession,
185
+ dataset_id: int,
186
+ changed: dict[int, tuple[str, str]],
187
+ actor_id: int,
188
+ ) -> tuple[dict[int, int], set[int]]:
189
+ if not changed:
190
+ return {}, set()
191
+ events = await latest_events(
192
+ session, dataset_id, {ref: previous for ref, (previous, _) in changed.items()}
193
+ )
194
+ carried: dict[int, int] = {}
195
+ now_unreviewed: set[int] = set()
196
+ for case_ref, (_, current) in changed.items():
197
+ decisive = events.decisive.get(case_ref)
198
+ if decisive is None:
199
+ continue
200
+ if decisive.kind == CaseEventKind.APPROVED:
201
+ now_unreviewed.add(case_ref)
202
+ continue
203
+ target = events.requested_to.get(case_ref) or decisive.actor_id
204
+ session.add(
205
+ models.CaseEvent(
206
+ dataset_id=dataset_id,
207
+ case_ref=case_ref,
208
+ content_hash=current,
209
+ kind=CaseEventKind.REVIEW_REQUESTED.value,
210
+ actor_id=actor_id,
211
+ target_id=target,
212
+ )
213
+ )
214
+ carried[case_ref] = target
215
+ return carried, now_unreviewed
216
+
217
+
218
+ class DatasetExists(ValueError):
219
+ def __init__(self, ref: str):
220
+ super().__init__(f"{ref} already exists")
221
+
222
+
223
+ async def create_dataset(session: AsyncSession, workspace: WorkspaceRef, name: str) -> DatasetRow:
224
+ check_name(name)
225
+ if await find_dataset_row(session, workspace.name, name) is not None:
226
+ raise DatasetExists(f"{name}@{workspace.name}")
227
+ session.add(models.Dataset(workspace_id=workspace.id, name=name, kind=DatasetKind.SUITE.value))
228
+ await session.flush()
229
+ return await get_dataset(session, workspace.name, name)
230
+
231
+
232
+ async def ensure_dataset(
233
+ session: AsyncSession, workspace: WorkspaceRef, name: str, kind: DatasetKind
234
+ ) -> int:
235
+ await session.execute(
236
+ insert(models.Dataset)
237
+ .values(workspace_id=workspace.id, name=name, kind=kind.value)
238
+ .on_conflict_do_nothing(index_elements=["workspace_id", "name"])
239
+ )
240
+ found = await session.scalar(
241
+ select(models.Dataset.id).where(
242
+ models.Dataset.workspace_id == workspace.id, models.Dataset.name == name
243
+ )
244
+ )
245
+ assert found is not None
246
+ return found
247
+
248
+
249
+ async def _usernames(session: AsyncSession, ids: set[int]) -> dict[int, str]:
250
+ if not ids:
251
+ return {}
252
+ rows = await session.execute(
253
+ select(models.User.id, models.User.username).where(models.User.id.in_(ids))
254
+ )
255
+ return {uid: name for uid, name in rows.all()}
256
+
257
+
258
+ async def _replace_overlays(
259
+ session: AsyncSession, dataset_id: int, overlays: list[IncomingOverlay]
260
+ ) -> int:
261
+ await session.execute(
262
+ delete(models.Overlay).where(models.Overlay.source_dataset_id == dataset_id)
263
+ )
264
+ targets = {
265
+ case_id: ref
266
+ for case_id, ref in (
267
+ await session.execute(
268
+ select(models.Case.case_id, models.Case.id).where(
269
+ models.Case.case_id.in_({o.target_case_id for o in overlays})
270
+ )
271
+ )
272
+ ).all()
273
+ }
274
+ applied = 0
275
+ for overlay in overlays:
276
+ target = targets.get(overlay.target_case_id)
277
+ if target is None:
278
+ continue
279
+ session.add(
280
+ models.Overlay(
281
+ source_dataset_id=dataset_id,
282
+ target_case=target,
283
+ agents=overlay.agents,
284
+ tags=overlay.tags,
285
+ evaluators=overlay.evaluators,
286
+ source_file=overlay.source_file,
287
+ )
288
+ )
289
+ applied += 1
290
+ return applied
291
+
292
+
293
+ async def sync_dataset(
294
+ session: AsyncSession,
295
+ *,
296
+ workspace: WorkspaceRef,
297
+ dataset: str,
298
+ kind: DatasetKind,
299
+ cases: list[IncomingCase],
300
+ overlays: list[IncomingOverlay],
301
+ full: bool,
302
+ actor_id: int,
303
+ source: SyncSource,
304
+ detail: dict,
305
+ ) -> SyncResult:
306
+ dataset_id = await ensure_dataset(session, workspace, dataset, kind)
307
+ sync = models.DatasetSync(
308
+ dataset_id=dataset_id, actor_id=actor_id, source=source.value, detail=dict(detail)
309
+ )
310
+ session.add(sync)
311
+ await session.flush()
312
+ result = SyncResult(sync_id=sync.id, dataset_id=dataset_id)
313
+ refs = await case_refs_for(session, [case.case_id for case in cases])
314
+ by_ref = {refs[case.case_id]: case for case in cases}
315
+ incoming = set(by_ref)
316
+ await store_versions(session, list(by_ref.items()))
317
+ existing = {
318
+ row.case_ref: row
319
+ for row in (
320
+ await session.scalars(
321
+ select(models.DatasetCase).where(models.DatasetCase.dataset_id == dataset_id)
322
+ )
323
+ ).all()
324
+ }
325
+ upserts: list[dict] = []
326
+ synced: list[models.CaseEvent] = []
327
+ changed: dict[int, tuple[str, str]] = {}
328
+ for case_ref, case in by_ref.items():
329
+ row = existing.get(case_ref)
330
+ same_version = (
331
+ row is not None and row.content_hash == case.content_hash and row.removed_at is None
332
+ )
333
+ if same_version:
334
+ result.unchanged += 1
335
+ else:
336
+ (result.created if row is None else result.patched).append(case.case_id)
337
+ synced.append(
338
+ models.CaseEvent(
339
+ dataset_id=dataset_id,
340
+ case_ref=case_ref,
341
+ content_hash=case.content_hash,
342
+ kind=CaseEventKind.SYNCED.value,
343
+ sync_id=sync.id,
344
+ actor_id=actor_id,
345
+ )
346
+ )
347
+ if row is not None and row.content_hash != case.content_hash:
348
+ changed[case_ref] = (row.content_hash, case.content_hash)
349
+ if (
350
+ same_version
351
+ and row is not None
352
+ and list(row.path_tags) == case.path_tags
353
+ and row.source == case.source
354
+ ):
355
+ continue
356
+ upserts.append(
357
+ {
358
+ "dataset_id": dataset_id,
359
+ "case_ref": case_ref,
360
+ "content_hash": case.content_hash,
361
+ "path_tags": case.path_tags,
362
+ "source": case.source,
363
+ "removed_at": None,
364
+ }
365
+ )
366
+ if upserts:
367
+ statement = insert(models.DatasetCase).values(upserts)
368
+ await session.execute(
369
+ statement.on_conflict_do_update(
370
+ index_elements=["dataset_id", "case_ref"],
371
+ set_={
372
+ "content_hash": statement.excluded.content_hash,
373
+ "path_tags": statement.excluded.path_tags,
374
+ "source": statement.excluded.source,
375
+ "removed_at": None,
376
+ "updated_at": func.now(),
377
+ },
378
+ )
379
+ )
380
+ session.add_all(synced)
381
+ carried_to, now_unreviewed = await _carry_or_reset(session, dataset_id, changed, actor_id)
382
+ carried = [
383
+ (case.case_id, carried_to[case_ref])
384
+ for case_ref, case in by_ref.items()
385
+ if case_ref in carried_to
386
+ ]
387
+ result.now_unreviewed = [
388
+ case.case_id for case_ref, case in by_ref.items() if case_ref in now_unreviewed
389
+ ]
390
+ if full:
391
+ stale = (
392
+ await session.execute(
393
+ select(models.DatasetCase.case_ref, models.Case.case_id)
394
+ .join(models.Case, models.Case.id == models.DatasetCase.case_ref)
395
+ .where(
396
+ models.DatasetCase.dataset_id == dataset_id,
397
+ models.DatasetCase.removed_at.is_(None),
398
+ models.DatasetCase.case_ref.not_in(incoming) if incoming else true(),
399
+ )
400
+ )
401
+ ).all()
402
+ if stale:
403
+ await session.execute(
404
+ update(models.DatasetCase)
405
+ .where(
406
+ models.DatasetCase.dataset_id == dataset_id,
407
+ models.DatasetCase.case_ref.in_([ref for ref, _ in stale]),
408
+ )
409
+ .values(removed_at=datetime.now(UTC))
410
+ )
411
+ result.removed = sorted(case_id for _, case_id in stale)
412
+ result.overlays = await _replace_overlays(session, dataset_id, overlays)
413
+ names = await _usernames(session, {target for _, target in carried})
414
+ result.carried = [CarriedReview(case_id=c, reviewer=names.get(t, "")) for c, t in carried]
415
+ sync.detail = {
416
+ **sync.detail,
417
+ "counts": {
418
+ "created": len(result.created),
419
+ "patched": len(result.patched),
420
+ "removed": len(result.removed),
421
+ "unchanged": result.unchanged,
422
+ },
423
+ }
424
+ return result
425
+
426
+
427
+ async def _overlay_blocks(
428
+ session: AsyncSession, workspace_id: int, case_refs: list[int]
429
+ ) -> dict[int, list[OverlayBlock]]:
430
+ if not case_refs:
431
+ return {}
432
+ rows = (
433
+ await session.execute(
434
+ select(models.Overlay, models.Dataset.name)
435
+ .join(models.Dataset, models.Dataset.id == models.Overlay.source_dataset_id)
436
+ .where(
437
+ models.Dataset.workspace_id == workspace_id,
438
+ models.Overlay.target_case.in_(case_refs),
439
+ )
440
+ )
441
+ ).all()
442
+ blocks: dict[int, list[OverlayBlock]] = {}
443
+ for overlay, source in rows:
444
+ blocks.setdefault(overlay.target_case, []).append(
445
+ OverlayBlock(
446
+ source=source,
447
+ agents=list(overlay.agents),
448
+ tags=list(overlay.tags),
449
+ evaluators=overlay.evaluators,
450
+ source_file=overlay.source_file,
451
+ )
452
+ )
453
+ return blocks
454
+
455
+
456
+ async def files_for(
457
+ session: AsyncSession, versions: list[tuple[int, str]]
458
+ ) -> dict[tuple[int, str], list[CaseFile]]:
459
+ if not versions:
460
+ return {}
461
+ rows = (
462
+ await session.scalars(
463
+ select(models.CaseVersionFile)
464
+ .where(
465
+ tuple_(models.CaseVersionFile.case_ref, models.CaseVersionFile.content_hash).in_(
466
+ versions
467
+ )
468
+ )
469
+ .order_by(models.CaseVersionFile.position)
470
+ )
471
+ ).all()
472
+ out: dict[tuple[int, str], list[CaseFile]] = {}
473
+ for row in rows:
474
+ out.setdefault((row.case_ref, row.content_hash), []).append(
475
+ CaseFile(key=row.key, sha256=row.sha256, context=row.context)
476
+ )
477
+ return out
478
+
479
+
480
+ async def case_views(
481
+ session: AsyncSession,
482
+ workspace_id: int,
483
+ versions: list[tuple[int, str]],
484
+ *,
485
+ agent: str | None,
486
+ dataset_id: int,
487
+ review_dataset_id: int | None = None,
488
+ ) -> list[CaseView]:
489
+ if not versions:
490
+ return []
491
+ rows = (
492
+ await session.execute(
493
+ select(models.CaseVersion, models.Case.case_id)
494
+ .join(models.Case, models.Case.id == models.CaseVersion.case_ref)
495
+ .where(
496
+ tuple_(models.CaseVersion.case_ref, models.CaseVersion.content_hash).in_(versions)
497
+ )
498
+ )
499
+ ).all()
500
+ by_version = {(v.case_ref, v.content_hash): (v, case_id) for v, case_id in rows}
501
+ placement = {
502
+ ref: (list(path_tags), source)
503
+ for ref, path_tags, source in (
504
+ await session.execute(
505
+ select(
506
+ models.DatasetCase.case_ref,
507
+ models.DatasetCase.path_tags,
508
+ models.DatasetCase.source,
509
+ ).where(
510
+ models.DatasetCase.dataset_id == dataset_id,
511
+ models.DatasetCase.case_ref.in_([ref for ref, _ in versions]),
512
+ )
513
+ )
514
+ ).all()
515
+ }
516
+ blocks = await _overlay_blocks(session, workspace_id, [ref for ref, _ in versions])
517
+ files = await files_for(session, versions)
518
+ states: dict[int, str] = {}
519
+ if review_dataset_id is not None:
520
+ events = await latest_events(session, review_dataset_id, dict(versions))
521
+ states = {
522
+ ref: (
523
+ CaseState(events.decisive[ref].kind).value
524
+ if ref in events.decisive
525
+ else CaseState.UNREVIEWED.value
526
+ )
527
+ for ref, _ in versions
528
+ }
529
+ views: list[CaseView] = []
530
+ for ref, content_hash in versions:
531
+ version, case_id = by_version[(ref, content_hash)]
532
+ case_blocks = blocks.get(ref, [])
533
+ overlay_tags = [tag for block in case_blocks for tag in block.tags]
534
+ path_tags, source = placement.get(ref, ([], None))
535
+ tags = list(dict.fromkeys([*version.tags, *path_tags]))
536
+ views.append(
537
+ CaseView(
538
+ case_ref=ref,
539
+ case_id=case_id,
540
+ content_hash=content_hash,
541
+ input=version.input,
542
+ expected=version.expected,
543
+ evaluators=version.evaluators,
544
+ effective_evaluators=apply_overlay_blocks(version.evaluators, case_blocks, agent),
545
+ tags=tags,
546
+ split_tags=list(dict.fromkeys([*tags, *overlay_tags])),
547
+ source=source,
548
+ files=files.get((ref, content_hash), []),
549
+ review_state=states.get(ref),
550
+ )
551
+ )
552
+ return sorted(views, key=lambda v: v.case_id)
553
+
554
+
555
+ async def current_versions(session: AsyncSession, dataset_id: int) -> list[tuple[int, str]]:
556
+ rows = await session.execute(
557
+ select(models.DatasetCase.case_ref, models.DatasetCase.content_hash).where(
558
+ models.DatasetCase.dataset_id == dataset_id, models.DatasetCase.removed_at.is_(None)
559
+ )
560
+ )
561
+ return [(ref, h) for ref, h in rows.all()]
562
+
563
+
564
+ async def current_cases(
565
+ session: AsyncSession, dataset: DatasetRow, *, agent: str | None = None
566
+ ) -> list[CaseView]:
567
+ workspace_id = await session.scalar(
568
+ select(models.Dataset.workspace_id).where(models.Dataset.id == dataset.id)
569
+ )
570
+ assert workspace_id is not None
571
+ reviewed = dataset.workspace_is_shared and dataset.kind == DatasetKind.SUITE
572
+ return await case_views(
573
+ session,
574
+ workspace_id,
575
+ await current_versions(session, dataset.id),
576
+ agent=agent,
577
+ dataset_id=dataset.id,
578
+ review_dataset_id=dataset.id if reviewed else None,
579
+ )
580
+
581
+
582
+ async def splits(session: AsyncSession, dataset: DatasetRow) -> list[tuple[str, int]]:
583
+ counts: dict[str, int] = {}
584
+ for view in await current_cases(session, dataset):
585
+ for tag in view.split_tags:
586
+ counts[tag] = counts.get(tag, 0) + 1
587
+ return sorted(counts.items())
588
+
589
+
590
+ async def workspace_case_index(
591
+ session: AsyncSession, workspace_id: int, *, exclude_dataset: int | None = None
592
+ ) -> dict[str, str]:
593
+ query = (
594
+ select(models.Case.case_id, models.Dataset.name)
595
+ .join(models.DatasetCase, models.DatasetCase.case_ref == models.Case.id)
596
+ .join(models.Dataset, models.Dataset.id == models.DatasetCase.dataset_id)
597
+ .where(
598
+ models.Dataset.workspace_id == workspace_id,
599
+ models.DatasetCase.removed_at.is_(None),
600
+ )
601
+ )
602
+ if exclude_dataset is not None:
603
+ query = query.where(models.Dataset.id != exclude_dataset)
604
+ return {case_id: name for case_id, name in (await session.execute(query)).all()}
605
+
606
+
607
+ def dataset_ref(row: DatasetRow) -> Reference:
608
+ return Reference(row.name, row.workspace)