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,271 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from sqlalchemy import delete, func, select
7
+ from sqlalchemy.dialects.postgresql import insert
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from evalkit.contracts.enums import SyncSource
11
+ from evalkit.core.access import WorkspaceRef
12
+ from evalkit.core.promotion import (
13
+ MissingTargetDataset,
14
+ OverlayCopy,
15
+ PromotionPlan,
16
+ twin_summary,
17
+ )
18
+ from evalkit.core.review import CaseEvent, CaseEventKind, CaseReview
19
+ from evalkit.db import models
20
+ from evalkit.db.cases import find_dataset_row
21
+ from evalkit.db.review_state import latest_events
22
+ from evalkit.db.workspaces import load_ref
23
+ from evalkit.store.base import CaseView, DatasetRow
24
+
25
+
26
+ async def overlays_extending(
27
+ session: AsyncSession, workspace_id: int, case_refs: list[int]
28
+ ) -> dict[int, list[OverlayCopy]]:
29
+ if not case_refs:
30
+ return {}
31
+ rows = (
32
+ await session.execute(
33
+ select(models.Overlay, models.Dataset.name)
34
+ .join(models.Dataset, models.Dataset.id == models.Overlay.source_dataset_id)
35
+ .where(
36
+ models.Dataset.workspace_id == workspace_id,
37
+ models.Overlay.target_case.in_(case_refs),
38
+ )
39
+ .order_by(models.Dataset.name)
40
+ )
41
+ ).all()
42
+ out: dict[int, list[OverlayCopy]] = {}
43
+ for overlay, source in rows:
44
+ out.setdefault(overlay.target_case, []).append(
45
+ OverlayCopy(
46
+ source_dataset=source,
47
+ agents=tuple(overlay.agents),
48
+ tags=tuple(overlay.tags),
49
+ evaluators=overlay.evaluators,
50
+ source_file=overlay.source_file,
51
+ )
52
+ )
53
+ return out
54
+
55
+
56
+ async def _workspace(session: AsyncSession, workspace_id: int) -> WorkspaceRef:
57
+ return await load_ref(session, await session.get_one(models.Workspace, workspace_id))
58
+
59
+
60
+ async def _username(session: AsyncSession, user_id: int | None) -> str | None:
61
+ if user_id is None:
62
+ return None
63
+ return await session.scalar(select(models.User.username).where(models.User.id == user_id))
64
+
65
+
66
+ async def _placement(
67
+ session: AsyncSession, dataset_id: int, case_ref: int
68
+ ) -> tuple[list[str], str | None]:
69
+ row = (
70
+ await session.execute(
71
+ select(models.DatasetCase.path_tags, models.DatasetCase.source).where(
72
+ models.DatasetCase.dataset_id == dataset_id,
73
+ models.DatasetCase.case_ref == case_ref,
74
+ )
75
+ )
76
+ ).one()
77
+ return list(row[0]), row[1]
78
+
79
+
80
+ async def plan_promotion(
81
+ session: AsyncSession, source_ws: WorkspaceRef, dataset: DatasetRow, case: CaseView
82
+ ) -> PromotionPlan:
83
+ assert source_ws.promotes_to is not None
84
+ target_ws = await load_ref(
85
+ session, await session.get_one(models.Workspace, source_ws.promotes_to)
86
+ )
87
+ target = await find_dataset_row(session, target_ws.name, dataset.name)
88
+ if target is None or target.kind != "suite":
89
+ raise MissingTargetDataset(f"{dataset.name}@{target_ws.name}")
90
+ overlays = (await overlays_extending(session, source_ws.id, [case.case_ref])).get(
91
+ case.case_ref, []
92
+ )
93
+ overlay_targets: dict[str, int] = {}
94
+ for name in sorted({o.source_dataset for o in overlays}):
95
+ found = await find_dataset_row(session, target_ws.name, name)
96
+ if found is None:
97
+ raise MissingTargetDataset(f"{name}@{target_ws.name}")
98
+ overlay_targets[name] = found.id
99
+ path_tags, source = await _placement(session, dataset.id, case.case_ref)
100
+ return PromotionPlan(
101
+ source_ref=dataset.ref,
102
+ target_ref=target.ref,
103
+ target_workspace_id=target_ws.id,
104
+ target_dataset_id=target.id,
105
+ case_ref=case.case_ref,
106
+ case_id=case.case_id,
107
+ content_hash=case.content_hash,
108
+ path_tags=path_tags,
109
+ source=source,
110
+ overlays=overlays,
111
+ overlay_targets=overlay_targets,
112
+ )
113
+
114
+
115
+ async def execute_promotion(
116
+ session: AsyncSession, plan: PromotionPlan, *, actor_id: int, event_id: int
117
+ ) -> int:
118
+ sync = models.DatasetSync(
119
+ dataset_id=plan.target_dataset_id,
120
+ actor_id=actor_id,
121
+ source=SyncSource.PROMOTION.value,
122
+ detail={
123
+ "from": plan.source_ref,
124
+ "case_id": plan.case_id,
125
+ "content_hash": plan.content_hash,
126
+ "event_id": event_id,
127
+ "overlays": len(plan.overlays),
128
+ },
129
+ )
130
+ session.add(sync)
131
+ await session.flush()
132
+ current = (
133
+ await session.execute(
134
+ select(models.DatasetCase.content_hash, models.DatasetCase.removed_at).where(
135
+ models.DatasetCase.dataset_id == plan.target_dataset_id,
136
+ models.DatasetCase.case_ref == plan.case_ref,
137
+ )
138
+ )
139
+ ).one_or_none()
140
+ if current is None or current[0] != plan.content_hash or current[1] is not None:
141
+ statement = insert(models.DatasetCase).values(
142
+ dataset_id=plan.target_dataset_id,
143
+ case_ref=plan.case_ref,
144
+ content_hash=plan.content_hash,
145
+ path_tags=plan.path_tags,
146
+ source=plan.source,
147
+ removed_at=None,
148
+ )
149
+ await session.execute(
150
+ statement.on_conflict_do_update(
151
+ index_elements=["dataset_id", "case_ref"],
152
+ set_={
153
+ "content_hash": statement.excluded.content_hash,
154
+ "path_tags": statement.excluded.path_tags,
155
+ "source": statement.excluded.source,
156
+ "removed_at": None,
157
+ "updated_at": func.now(),
158
+ },
159
+ )
160
+ )
161
+ for kind, sync_id in (
162
+ (CaseEventKind.SYNCED, sync.id),
163
+ (CaseEventKind.APPROVED, None),
164
+ ):
165
+ session.add(
166
+ models.CaseEvent(
167
+ dataset_id=plan.target_dataset_id,
168
+ case_ref=plan.case_ref,
169
+ content_hash=plan.content_hash,
170
+ kind=kind.value,
171
+ sync_id=sync_id,
172
+ actor_id=actor_id,
173
+ )
174
+ )
175
+ await session.flush()
176
+ await session.execute(
177
+ delete(models.Overlay).where(
178
+ models.Overlay.target_case == plan.case_ref,
179
+ models.Overlay.source_dataset_id.in_(
180
+ select(models.Dataset.id).where(
181
+ models.Dataset.workspace_id == plan.target_workspace_id
182
+ )
183
+ ),
184
+ )
185
+ )
186
+ for overlay in plan.overlays:
187
+ session.add(
188
+ models.Overlay(
189
+ source_dataset_id=plan.overlay_targets[overlay.source_dataset],
190
+ target_case=plan.case_ref,
191
+ agents=list(overlay.agents),
192
+ tags=list(overlay.tags),
193
+ evaluators=overlay.evaluators,
194
+ source_file=overlay.source_file,
195
+ )
196
+ )
197
+ await session.flush()
198
+ return sync.id
199
+
200
+
201
+ async def _review_of(
202
+ session: AsyncSession, dataset: DatasetRow, case_ref: int, case_id: str, content_hash: str
203
+ ) -> CaseReview:
204
+ events = await latest_events(session, dataset.id, {case_ref: content_hash})
205
+ review = CaseReview.unreviewed(dataset.ref, case_id, content_hash)
206
+ row = events.decisive.get(case_ref)
207
+ if row is not None:
208
+ actor = await _username(session, row.actor_id)
209
+ assert actor is not None
210
+ review.last_event = CaseEvent(
211
+ id=row.id,
212
+ dataset=dataset.ref,
213
+ case_id=case_id,
214
+ content_hash=row.content_hash,
215
+ kind=CaseEventKind(row.kind),
216
+ actor=actor,
217
+ target=await _username(session, row.target_id),
218
+ created_at=row.created_at,
219
+ )
220
+ review.requested_to = await _username(session, events.requested_to.get(case_ref))
221
+ return review
222
+
223
+
224
+ async def twin_status(
225
+ session: AsyncSession, ws: WorkspaceRef, dataset: DatasetRow, cases: list[CaseView]
226
+ ) -> dict[str, dict[str, Any] | None]:
227
+ other_id = ws.promotes_to if ws.promotes_to is not None else ws.promoted_from
228
+ if other_id is None or dataset.kind != "suite" or not cases:
229
+ return {case.case_id: None for case in cases}
230
+ other_ws = await _workspace(session, other_id)
231
+ other = await find_dataset_row(session, other_ws.name, dataset.name)
232
+ refs = [case.case_ref for case in cases]
233
+ theirs: dict[int, tuple[str, datetime | None]] = {}
234
+ if other is not None:
235
+ theirs = {
236
+ ref: (content_hash, removed_at)
237
+ for ref, content_hash, removed_at in (
238
+ await session.execute(
239
+ select(
240
+ models.DatasetCase.case_ref,
241
+ models.DatasetCase.content_hash,
242
+ models.DatasetCase.removed_at,
243
+ ).where(
244
+ models.DatasetCase.dataset_id == other.id,
245
+ models.DatasetCase.case_ref.in_(refs),
246
+ )
247
+ )
248
+ ).all()
249
+ }
250
+ mine_overlays = await overlays_extending(session, ws.id, refs)
251
+ their_overlays = await overlays_extending(session, other_ws.id, refs)
252
+ out: dict[str, dict[str, Any] | None] = {}
253
+ for case in cases:
254
+ found = theirs.get(case.case_ref)
255
+ other_hash = found[0] if found is not None and found[1] is None else None
256
+ review = (
257
+ await _review_of(session, other, case.case_ref, case.case_id, other_hash)
258
+ if other is not None and other_hash is not None
259
+ else None
260
+ )
261
+ out[case.case_id] = twin_summary(
262
+ workspace=other_ws.name,
263
+ dataset=dataset.name,
264
+ case_id=case.case_id,
265
+ mine=case.content_hash,
266
+ other=other_hash,
267
+ review=review,
268
+ my_overlays=mine_overlays.get(case.case_ref, []),
269
+ other_overlays=their_overlays.get(case.case_ref, []),
270
+ )
271
+ return out
evalkit/db/rekey.py ADDED
@@ -0,0 +1,46 @@
1
+ from sqlalchemy import func, select
2
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
3
+
4
+ from evalkit.db import models
5
+ from evalkit.db.crypto import SecretBox
6
+
7
+ type Sealed = models.TraceSource | models.Agent | models.RemoteEvaluator | models.LlmProvider
8
+
9
+ SEALED: tuple[type[Sealed], ...] = (
10
+ models.TraceSource,
11
+ models.Agent,
12
+ models.RemoteEvaluator,
13
+ models.LlmProvider,
14
+ )
15
+
16
+
17
+ async def count_sealed(session_factory: async_sessionmaker[AsyncSession]) -> int:
18
+ count = 0
19
+ async with session_factory() as session:
20
+ for model in SEALED:
21
+ count += int(
22
+ await session.scalar(
23
+ select(func.count()).where(model.api_key_encrypted.is_not(None))
24
+ )
25
+ or 0
26
+ )
27
+ return count
28
+
29
+
30
+ async def _rekey[T: Sealed](
31
+ session: AsyncSession, model: type[T], old: SecretBox, new: SecretBox
32
+ ) -> int:
33
+ rows = (await session.scalars(select(model).where(model.api_key_encrypted.is_not(None)))).all()
34
+ for row in rows:
35
+ assert row.api_key_encrypted is not None
36
+ row.api_key_encrypted = new.seal(old.open(row.api_key_encrypted))
37
+ return len(rows)
38
+
39
+
40
+ async def rekey_all(
41
+ session_factory: async_sessionmaker[AsyncSession], old: SecretBox, new: SecretBox
42
+ ) -> int:
43
+ async with session_factory() as session:
44
+ count = sum([await _rekey(session, model, old, new) for model in SEALED])
45
+ await session.commit()
46
+ return count
evalkit/db/review.py ADDED
@@ -0,0 +1,250 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+
5
+ from sqlalchemy import select, update
6
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
7
+
8
+ from evalkit.core.review import (
9
+ CaseComment,
10
+ CaseEvent,
11
+ CaseEventKind,
12
+ CaseReview,
13
+ CaseVersion,
14
+ NewCaseComment,
15
+ NewCaseEvent,
16
+ )
17
+ from evalkit.core.workspace import Reference, slug
18
+ from evalkit.db import models
19
+ from evalkit.db.cases import dataset_by_ref
20
+ from evalkit.db.review_state import latest_events
21
+
22
+ AfterDecision = Callable[[AsyncSession, int], Awaitable[None]]
23
+
24
+
25
+ async def _case_refs(session: AsyncSession, case_ids: list[str]) -> dict[str, int]:
26
+ rows = await session.execute(
27
+ select(models.Case.case_id, models.Case.id).where(models.Case.case_id.in_(case_ids))
28
+ )
29
+ return {case_id: ref for case_id, ref in rows.all()}
30
+
31
+
32
+ async def _user_ids(session: AsyncSession, names: set[str]) -> dict[str, int]:
33
+ rows = await session.execute(
34
+ select(models.User.username, models.User.id).where(models.User.username.in_(names))
35
+ )
36
+ return {name: uid for name, uid in rows.all()}
37
+
38
+
39
+ async def _usernames(session: AsyncSession, ids: set[int]) -> dict[int, str]:
40
+ rows = await session.execute(
41
+ select(models.User.id, models.User.username).where(models.User.id.in_(ids))
42
+ )
43
+ return {uid: name for uid, name in rows.all()}
44
+
45
+
46
+ class ReviewLog:
47
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
48
+ self._session_factory = session_factory
49
+
50
+ async def _events(
51
+ self, session: AsyncSession, dataset: str, case_id: str, rows: list[models.CaseEvent]
52
+ ) -> list[CaseEvent]:
53
+ users = {r.actor_id for r in rows} | {r.target_id for r in rows if r.target_id}
54
+ names = await _usernames(session, users)
55
+ return [
56
+ CaseEvent(
57
+ id=r.id,
58
+ dataset=dataset,
59
+ case_id=case_id,
60
+ content_hash=r.content_hash,
61
+ kind=CaseEventKind(r.kind),
62
+ actor=names[r.actor_id],
63
+ target=names[r.target_id] if r.target_id else None,
64
+ created_at=r.created_at,
65
+ )
66
+ for r in rows
67
+ ]
68
+
69
+ async def _comment(
70
+ self, session: AsyncSession, row: models.CaseComment, dataset: str, case_id: str
71
+ ) -> CaseComment:
72
+ names = await _usernames(session, {row.author_id})
73
+ return CaseComment(
74
+ id=row.id,
75
+ dataset=dataset,
76
+ case_id=case_id,
77
+ content_hash=row.content_hash,
78
+ parent_id=row.parent_id,
79
+ author=names[row.author_id],
80
+ body=row.body,
81
+ resolved=row.resolved,
82
+ created_at=row.created_at,
83
+ )
84
+
85
+ async def load(self, dataset: str, versions: list[CaseVersion]) -> dict[str, CaseReview]:
86
+ if not versions:
87
+ return {}
88
+ reviews = {
89
+ v.case_id: CaseReview.unreviewed(dataset, v.case_id, v.content_hash) for v in versions
90
+ }
91
+ async with self._session_factory() as session:
92
+ dataset_id = (await dataset_by_ref(session, dataset)).id
93
+ refs = await _case_refs(session, [v.case_id for v in versions])
94
+ ids = {ref: case_id for case_id, ref in refs.items()}
95
+ latest = await latest_events(
96
+ session,
97
+ dataset_id,
98
+ {refs[v.case_id]: v.content_hash for v in versions if v.case_id in refs},
99
+ )
100
+ for ref, row in latest.decisive.items():
101
+ [event] = await self._events(session, dataset, ids[ref], [row])
102
+ reviews[ids[ref]].last_event = event
103
+ names = await _usernames(session, set(latest.requested_to.values()))
104
+ for ref, target in latest.requested_to.items():
105
+ reviews[ids[ref]].requested_to = names[target]
106
+ return reviews
107
+
108
+ async def latest_hashes(self, dataset: str, case_ids: list[str]) -> dict[str, str]:
109
+ if not case_ids:
110
+ return {}
111
+ async with self._session_factory() as session:
112
+ dataset_id = (await dataset_by_ref(session, dataset)).id
113
+ rows = await session.execute(
114
+ select(models.Case.case_id, models.DatasetCase.content_hash)
115
+ .join(models.Case, models.Case.id == models.DatasetCase.case_ref)
116
+ .where(
117
+ models.DatasetCase.dataset_id == dataset_id,
118
+ models.Case.case_id.in_(case_ids),
119
+ )
120
+ )
121
+ return {case_id: content_hash for case_id, content_hash in rows.all()}
122
+
123
+ async def _add_events(
124
+ self, session: AsyncSession, events: list[NewCaseEvent]
125
+ ) -> list[models.CaseEvent]:
126
+ refs = await _case_refs(session, [e.case_id for e in events])
127
+ users = await _user_ids(
128
+ session, {e.actor for e in events} | {e.target for e in events if e.target}
129
+ )
130
+ rows: list[models.CaseEvent] = []
131
+ for event in events:
132
+ dataset_id = (await dataset_by_ref(session, event.dataset)).id
133
+ row = models.CaseEvent(
134
+ dataset_id=dataset_id,
135
+ case_ref=refs[event.case_id],
136
+ content_hash=event.content_hash,
137
+ kind=event.kind.value,
138
+ actor_id=users[event.actor],
139
+ target_id=users[event.target] if event.target else None,
140
+ )
141
+ session.add(row)
142
+ await session.flush()
143
+ rows.append(row)
144
+ return rows
145
+
146
+ async def _comment_row(
147
+ self, session: AsyncSession, comment: NewCaseComment
148
+ ) -> models.CaseComment:
149
+ dataset_id = (await dataset_by_ref(session, comment.dataset)).id
150
+ refs = await _case_refs(session, [comment.case_id])
151
+ users = await _user_ids(session, {comment.author})
152
+ return models.CaseComment(
153
+ dataset_id=dataset_id,
154
+ case_ref=refs[comment.case_id],
155
+ content_hash=comment.content_hash,
156
+ parent_id=comment.parent_id,
157
+ author_id=users[comment.author],
158
+ body=comment.body,
159
+ )
160
+
161
+ async def append(self, events: list[NewCaseEvent]) -> None:
162
+ if not events:
163
+ return
164
+ async with self._session_factory() as session:
165
+ await self._add_events(session, events)
166
+ await session.commit()
167
+
168
+ async def decide(
169
+ self,
170
+ event: NewCaseEvent,
171
+ comment: NewCaseComment | None,
172
+ after: AfterDecision | None = None,
173
+ ) -> None:
174
+ async with self._session_factory() as session:
175
+ if comment is not None:
176
+ session.add(await self._comment_row(session, comment))
177
+ [row] = await self._add_events(session, [event])
178
+ if after is not None:
179
+ await after(session, row.id)
180
+ await session.commit()
181
+
182
+ async def history(self, dataset: str, case_id: str) -> list[CaseEvent]:
183
+ async with self._session_factory() as session:
184
+ dataset_id = (await dataset_by_ref(session, dataset)).id
185
+ rows = (
186
+ await session.scalars(
187
+ select(models.CaseEvent)
188
+ .join(models.Case, models.Case.id == models.CaseEvent.case_ref)
189
+ .where(
190
+ models.CaseEvent.dataset_id == dataset_id, models.Case.case_id == case_id
191
+ )
192
+ .order_by(models.CaseEvent.created_at.desc(), models.CaseEvent.id.desc())
193
+ )
194
+ ).all()
195
+ return await self._events(session, dataset, case_id, list(rows))
196
+
197
+ async def comments(self, dataset: str, case_id: str) -> list[CaseComment]:
198
+ async with self._session_factory() as session:
199
+ dataset_id = (await dataset_by_ref(session, dataset)).id
200
+ rows = (
201
+ await session.scalars(
202
+ select(models.CaseComment)
203
+ .join(models.Case, models.Case.id == models.CaseComment.case_ref)
204
+ .where(
205
+ models.CaseComment.dataset_id == dataset_id,
206
+ models.Case.case_id == case_id,
207
+ )
208
+ .order_by(models.CaseComment.created_at.asc(), models.CaseComment.id.asc())
209
+ )
210
+ ).all()
211
+ return [await self._comment(session, row, dataset, case_id) for row in rows]
212
+
213
+ async def add_comment(self, comment: NewCaseComment) -> CaseComment:
214
+ async with self._session_factory() as session:
215
+ row = await self._comment_row(session, comment)
216
+ session.add(row)
217
+ await session.commit()
218
+ await session.refresh(row)
219
+ return await self._comment(session, row, comment.dataset, comment.case_id)
220
+
221
+ async def _describe(self, session: AsyncSession, row: models.CaseComment) -> CaseComment:
222
+ name, workspace = (
223
+ await session.execute(
224
+ select(models.Dataset.name, models.Workspace.name)
225
+ .join(models.Workspace, models.Workspace.id == models.Dataset.workspace_id)
226
+ .where(models.Dataset.id == row.dataset_id)
227
+ )
228
+ ).one()
229
+ case_id = await session.scalar(
230
+ select(models.Case.case_id).where(models.Case.id == row.case_ref)
231
+ )
232
+ assert case_id is not None
233
+ return await self._comment(session, row, slug(Reference(name, workspace)), case_id)
234
+
235
+ async def get_comment(self, comment_id: int) -> CaseComment | None:
236
+ async with self._session_factory() as session:
237
+ row = await session.get(models.CaseComment, comment_id)
238
+ return None if row is None else await self._describe(session, row)
239
+
240
+ async def set_resolved(self, comment_id: int, resolved: bool) -> CaseComment:
241
+ async with self._session_factory() as session:
242
+ await session.execute(
243
+ update(models.CaseComment)
244
+ .where(models.CaseComment.id == comment_id)
245
+ .values(resolved=resolved)
246
+ )
247
+ await session.commit()
248
+ row = await session.get(models.CaseComment, comment_id)
249
+ assert row is not None
250
+ return await self._describe(session, row)
@@ -0,0 +1,50 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from sqlalchemy import select, tuple_
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from evalkit.core.review import DECISIVE_EVENT_KINDS, CaseEventKind
7
+ from evalkit.db import models
8
+
9
+
10
+ @dataclass
11
+ class LatestEvents:
12
+ decisive: dict[int, models.CaseEvent] = field(default_factory=dict)
13
+ requested_to: dict[int, int] = field(default_factory=dict)
14
+
15
+
16
+ async def _latest(
17
+ session: AsyncSession, dataset_id: int, versions: dict[int, str], kinds: list[str]
18
+ ) -> dict[int, models.CaseEvent]:
19
+ rows = await session.scalars(
20
+ select(models.CaseEvent)
21
+ .where(
22
+ models.CaseEvent.dataset_id == dataset_id,
23
+ tuple_(models.CaseEvent.case_ref, models.CaseEvent.content_hash).in_(
24
+ list(versions.items())
25
+ ),
26
+ models.CaseEvent.kind.in_(kinds),
27
+ )
28
+ .distinct(models.CaseEvent.case_ref)
29
+ .order_by(
30
+ models.CaseEvent.case_ref,
31
+ models.CaseEvent.created_at.desc(),
32
+ models.CaseEvent.id.desc(),
33
+ )
34
+ )
35
+ return {row.case_ref: row for row in rows.all()}
36
+
37
+
38
+ async def latest_events(
39
+ session: AsyncSession, dataset_id: int, versions: dict[int, str]
40
+ ) -> LatestEvents:
41
+ if not versions:
42
+ return LatestEvents()
43
+ decisive = await _latest(session, dataset_id, versions, [k.value for k in DECISIVE_EVENT_KINDS])
44
+ requests = await _latest(session, dataset_id, versions, [CaseEventKind.REVIEW_REQUESTED.value])
45
+ return LatestEvents(
46
+ decisive=decisive,
47
+ requested_to={
48
+ ref: event.target_id for ref, event in requests.items() if event.target_id is not None
49
+ },
50
+ )