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,94 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+ from typing import Any
6
+
7
+ from evalkit.core.review import CaseReview, review_summary
8
+
9
+ __all__ = [
10
+ "MissingTargetDataset",
11
+ "OverlayCopy",
12
+ "PromotionPlan",
13
+ "PromotionTarget",
14
+ "TwinState",
15
+ "twin_state",
16
+ "twin_summary",
17
+ ]
18
+
19
+
20
+ class MissingTargetDataset(Exception):
21
+ def __init__(self, ref: str):
22
+ self.ref = ref
23
+ super().__init__(
24
+ f"{ref} does not exist; create it before approving (evalkit datasets create)"
25
+ )
26
+
27
+
28
+ class PromotionTarget(Exception):
29
+ def __init__(self, workspace: str, source: str, hint: str):
30
+ self.workspace = workspace
31
+ self.source = source
32
+ super().__init__(f"{workspace} is the promotion target of {source}; {hint}")
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class OverlayCopy:
37
+ source_dataset: str
38
+ agents: tuple[str, ...]
39
+ tags: tuple[str, ...]
40
+ evaluators: dict[str, Any]
41
+ source_file: str | None
42
+
43
+ def key(self) -> tuple[str, tuple[str, ...], tuple[str, ...], str]:
44
+ return (self.source_dataset, self.agents, self.tags, repr(sorted(self.evaluators.items())))
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class PromotionPlan:
49
+ source_ref: str
50
+ target_ref: str
51
+ target_workspace_id: int
52
+ target_dataset_id: int
53
+ case_ref: int
54
+ case_id: str
55
+ content_hash: str
56
+ path_tags: list[str]
57
+ source: str | None
58
+ overlays: list[OverlayCopy]
59
+ overlay_targets: dict[str, int]
60
+
61
+
62
+ class TwinState(StrEnum):
63
+ ABSENT = "absent"
64
+ SAME = "same"
65
+ DIFFERS = "differs"
66
+
67
+
68
+ def twin_state(mine: str, other: str | None) -> TwinState:
69
+ if other is None:
70
+ return TwinState.ABSENT
71
+ return TwinState.SAME if mine == other else TwinState.DIFFERS
72
+
73
+
74
+ def twin_summary(
75
+ *,
76
+ workspace: str,
77
+ dataset: str,
78
+ case_id: str,
79
+ mine: str,
80
+ other: str | None,
81
+ review: CaseReview | None,
82
+ my_overlays: list[OverlayCopy],
83
+ other_overlays: list[OverlayCopy],
84
+ ) -> dict[str, Any]:
85
+ state = twin_state(mine, other)
86
+ return {
87
+ "workspace": workspace,
88
+ "state": state.value,
89
+ "content_hash": other,
90
+ "review": review_summary(review) if review is not None else None,
91
+ "overlays_differ": state is not TwinState.ABSENT
92
+ and {o.key() for o in my_overlays} != {o.key() for o in other_overlays},
93
+ "href": f"/datasets/{dataset}@{workspace}/cases/{case_id}",
94
+ }
evalkit/core/pull.py ADDED
@@ -0,0 +1,198 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from collections.abc import Awaitable, Callable
6
+ from datetime import datetime
7
+ from typing import Literal
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from evalkit.contracts.enums import EvaluatorKind
12
+ from evalkit.contracts.harness import RunListItem, RunListParams
13
+ from evalkit.core.harness import HarnessClient
14
+ from evalkit.store.base import AgentRunRecord, EvaluationRow, RunEvalContext
15
+
16
+ UNSAFE = re.compile(r"[^A-Za-z0-9._-]")
17
+ MAX_PULL = 100
18
+ HARNESS_PAGE = 200
19
+ MAX_HARNESS_SCAN = 10000
20
+
21
+ Exclude = Callable[[list[str]], Awaitable[set[str]]]
22
+
23
+
24
+ class HarnessRuns(BaseModel):
25
+ runs: list[RunListItem]
26
+ total: int
27
+ capped: bool = False
28
+
29
+
30
+ SCAN_NOTE = (
31
+ f"stopped after scanning {MAX_HARNESS_SCAN} harness runs; narrow the window with since/until"
32
+ )
33
+
34
+
35
+ class RunBundle(BaseModel):
36
+ run_id: str
37
+ source: Literal["app", "eval"]
38
+ status: str | None = None
39
+ created_at: datetime | None = None
40
+ title: str | None = None
41
+ run: dict
42
+ trajectory: dict | None = None
43
+ eval: RunEvalContext | None = None
44
+ pull_error: str | None = None
45
+
46
+
47
+ def in_window(run: RunListItem, since: datetime | None, until: datetime | None) -> bool:
48
+ if run.created_at is None:
49
+ return True
50
+ return (since is None or run.created_at >= since) and (until is None or run.created_at < until)
51
+
52
+
53
+ async def list_harness_runs(
54
+ harness: HarnessClient,
55
+ *,
56
+ wanted: int,
57
+ since: datetime | None = None,
58
+ until: datetime | None = None,
59
+ query: str | None = None,
60
+ user_id: str | None = None,
61
+ client_id: str | None = None,
62
+ exclude: Exclude | None = None,
63
+ ) -> HarnessRuns:
64
+ kept: list[RunListItem] = []
65
+ sessions: set[str] = set()
66
+ dropped = offset = 0
67
+ size = HARNESS_PAGE
68
+
69
+ def new_session(run: RunListItem) -> bool:
70
+ if run.session_id is None:
71
+ return True
72
+ if run.session_id in sessions:
73
+ return False
74
+ sessions.add(run.session_id)
75
+ return True
76
+
77
+ while True:
78
+ page = await harness.list_runs(
79
+ RunListParams(
80
+ query=query,
81
+ user_id=user_id,
82
+ client_id=client_id,
83
+ limit=size,
84
+ offset=offset,
85
+ since=since,
86
+ until=until,
87
+ )
88
+ )
89
+ window = [run for run in page.runs if in_window(run, since, until)]
90
+ excluded = await exclude([run.run_id for run in window]) if exclude and window else set()
91
+ fresh = [run for run in window if run.run_id not in excluded and new_session(run)]
92
+ kept += fresh
93
+ dropped += len(page.runs) - len(fresh)
94
+ offset += len(page.runs)
95
+ oldest = page.runs[-1].created_at if page.runs else None
96
+ # A short page is the end only when the harness says so; a harness that
97
+ # caps its page below `size` keeps being paged at its own size.
98
+ size = min(size, len(page.runs)) or size
99
+ if (
100
+ not page.runs
101
+ or offset >= page.total
102
+ or (since is not None and oldest is not None and oldest < since)
103
+ ):
104
+ return HarnessRuns(runs=kept, total=len(kept))
105
+ if len(kept) >= wanted:
106
+ return HarnessRuns(runs=kept, total=max(page.total - dropped, len(kept)))
107
+ if offset >= MAX_HARNESS_SCAN:
108
+ return HarnessRuns(runs=kept, total=max(page.total - dropped, len(kept)), capped=True)
109
+
110
+
111
+ def safe_dir(run_id: str) -> str:
112
+ return UNSAFE.sub("_", run_id)
113
+
114
+
115
+ def run_body(
116
+ run_id: str,
117
+ source: Literal["app", "eval"],
118
+ record: AgentRunRecord | None,
119
+ eval_context: RunEvalContext | None,
120
+ ) -> dict:
121
+ body: dict = {"run_id": run_id, "source": source}
122
+ if eval_context is not None:
123
+ body["input"] = eval_context.case.input
124
+ if record is not None:
125
+ body |= {
126
+ "status": record.status.value,
127
+ "answer": record.answer,
128
+ "output": record.output,
129
+ "error": record.error,
130
+ "started_at": record.started_at.isoformat(),
131
+ "ended_at": record.ended_at.isoformat() if record.ended_at else None,
132
+ "metrics": record.metrics,
133
+ }
134
+ return body
135
+
136
+
137
+ def scores(evaluations: list[EvaluationRow]) -> dict[str, float]:
138
+ return {
139
+ row.name: row.value
140
+ for row in evaluations
141
+ if row.kind is EvaluatorKind.SCORE and row.error is None and row.value is not None
142
+ }
143
+
144
+
145
+ def index_row(bundle: RunBundle) -> dict:
146
+ context = bundle.eval
147
+ return {
148
+ "run_id": bundle.run_id,
149
+ "dir": f"runs/{safe_dir(bundle.run_id)}",
150
+ "source": bundle.source,
151
+ "status": bundle.status,
152
+ "created_at": bundle.created_at.isoformat() if bundle.created_at else None,
153
+ "title": bundle.title,
154
+ "experiment_id": context.experiment_id if context else None,
155
+ "case_id": context.case.case_id if context else None,
156
+ "repetition": context.repetition if context else None,
157
+ "scores": scores(context.evaluations) if context else None,
158
+ "error": bundle.run.get("error"),
159
+ "pull_error": bundle.pull_error,
160
+ }
161
+
162
+
163
+ def _json(doc: object) -> bytes:
164
+ return json.dumps(doc, indent=2, ensure_ascii=False, default=str).encode()
165
+
166
+
167
+ def bundle_files(bundle: RunBundle) -> dict[str, bytes]:
168
+ base = f"runs/{safe_dir(bundle.run_id)}"
169
+ run = (
170
+ bundle.run if bundle.pull_error is None else {**bundle.run, "pull_error": bundle.pull_error}
171
+ )
172
+ files = {f"{base}/run.json": _json(run)}
173
+ if bundle.trajectory is not None:
174
+ files[f"{base}/trajectory.json"] = _json(bundle.trajectory)
175
+ if bundle.eval is not None:
176
+ case = bundle.eval.case
177
+ files[f"{base}/case.json"] = _json(
178
+ {
179
+ "case_id": case.case_id,
180
+ "content_hash": case.content_hash,
181
+ "input": case.input,
182
+ "expected": case.expected,
183
+ "evaluators": case.effective_evaluators,
184
+ "tags": case.tags,
185
+ }
186
+ )
187
+ files[f"{base}/evaluations.json"] = _json(
188
+ [row.model_dump(mode="json", exclude_none=True) for row in bundle.eval.evaluations]
189
+ )
190
+ return files
191
+
192
+
193
+ def merge_index(existing: dict | None, fresh: dict) -> dict:
194
+ if existing is None:
195
+ return fresh
196
+ replaced = {row["run_id"] for row in fresh["runs"]}
197
+ kept = [row for row in existing["runs"] if row["run_id"] not in replaced]
198
+ return {**fresh, "runs": kept + fresh["runs"]}
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import logging
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from typing import Any, Literal
9
+
10
+ from evalkit.contracts.custom_evaluator import EvalCase, EvalRun, ScoreItem
11
+ from evalkit.core.custom_eval import EvalError
12
+
13
+ LOGGER = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class PythonEvaluator:
18
+ name: str
19
+ kind: Literal["evaluator", "gauge"]
20
+ fn: Callable[[EvalCase, EvalRun], Any]
21
+ requires_trajectory: bool = False
22
+
23
+ def __call__(self, case: EvalCase, run: EvalRun) -> Any:
24
+ return self.fn(case, run)
25
+
26
+
27
+ def _normalise(name: str, result: Any) -> list[ScoreItem]:
28
+ if isinstance(result, ScoreItem):
29
+ return [result]
30
+ if isinstance(result, int | float) and not isinstance(result, bool):
31
+ return [ScoreItem(name=name, score=float(result))]
32
+ if isinstance(result, list) and all(isinstance(item, ScoreItem) for item in result):
33
+ return result
34
+ raise TypeError(
35
+ f"evaluator {name} returned {type(result).__name__}; "
36
+ "expected float, ScoreItem or list[ScoreItem]"
37
+ )
38
+
39
+
40
+ async def score_with_python_evaluator(
41
+ evaluator: PythonEvaluator,
42
+ *,
43
+ case: EvalCase,
44
+ answer: str,
45
+ output: dict | None,
46
+ trajectory: dict,
47
+ ) -> list[ScoreItem] | EvalError:
48
+ run = EvalRun(answer=answer, output=output, trajectory=trajectory)
49
+ try:
50
+ if inspect.iscoroutinefunction(evaluator.fn):
51
+ result = await evaluator.fn(case, run)
52
+ else:
53
+ result = await asyncio.to_thread(evaluator.fn, case, run)
54
+ if inspect.isawaitable(result):
55
+ result = await result
56
+ return _normalise(evaluator.name, result)
57
+ except Exception as exc:
58
+ LOGGER.warning("python evaluator %s errored: %s", evaluator.name, exc)
59
+ return EvalError(evaluator=evaluator.name, message=f"{type(exc).__name__}: {exc}")
@@ -0,0 +1,32 @@
1
+ from typing import Literal, Protocol
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from evalkit.contracts.enums import EvaluatorKind
6
+ from evalkit.contracts.settings import RemoteEvaluatorSettings
7
+
8
+
9
+ class RemoteEvaluator(BaseModel):
10
+ name: str
11
+ workspace: str
12
+ kind: EvaluatorKind
13
+ settings: RemoteEvaluatorSettings
14
+ archived: bool = False
15
+ has_api_key: bool = False
16
+
17
+ @property
18
+ def ref(self) -> str:
19
+ return f"{self.name}@{self.workspace}"
20
+
21
+ @property
22
+ def wire_kind(self) -> Literal["evaluator", "gauge"]:
23
+ return "evaluator" if self.kind is EvaluatorKind.SCORE else "gauge"
24
+
25
+
26
+ class EvaluatorLookup(Protocol):
27
+ async def in_workspace(self, workspace: str) -> dict[str, RemoteEvaluator]: ...
28
+
29
+
30
+ class UnknownEvaluator(KeyError):
31
+ def __init__(self, ref: str):
32
+ super().__init__(f"unknown evaluator: {ref} — create it with evalkit evaluators create")
evalkit/core/review.py ADDED
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ from enum import StrEnum
7
+ from typing import Any, NamedTuple, Protocol
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from evalkit.store.base import CaseView, DatasetRow
12
+
13
+ __all__ = [
14
+ "DECISIVE_EVENT_KINDS",
15
+ "OPEN_CASE_STATES",
16
+ "CaseComment",
17
+ "CaseEvent",
18
+ "CaseEventKind",
19
+ "CaseReview",
20
+ "CaseState",
21
+ "CaseVersion",
22
+ "NewCaseComment",
23
+ "NewCaseEvent",
24
+ "NotReviewable",
25
+ "ReviewLogProtocol",
26
+ "ensure_reviewable",
27
+ "is_reviewable",
28
+ "group_threads",
29
+ "review_summary",
30
+ ]
31
+
32
+
33
+ class CaseEventKind(StrEnum):
34
+ SYNCED = "synced"
35
+ REVIEW_REQUESTED = "review_requested"
36
+ APPROVED = "approved"
37
+ CHANGES_REQUESTED = "changes_requested"
38
+
39
+
40
+ class CaseState(StrEnum):
41
+ UNREVIEWED = "unreviewed"
42
+ REVIEW_REQUESTED = "review_requested"
43
+ CHANGES_REQUESTED = "changes_requested"
44
+ APPROVED = "approved"
45
+
46
+
47
+ DECISIVE_EVENT_KINDS = frozenset(
48
+ {
49
+ CaseEventKind.REVIEW_REQUESTED,
50
+ CaseEventKind.APPROVED,
51
+ CaseEventKind.CHANGES_REQUESTED,
52
+ }
53
+ )
54
+
55
+ _CASE_STATE_FOR = {
56
+ CaseEventKind.REVIEW_REQUESTED: CaseState.REVIEW_REQUESTED,
57
+ CaseEventKind.APPROVED: CaseState.APPROVED,
58
+ CaseEventKind.CHANGES_REQUESTED: CaseState.CHANGES_REQUESTED,
59
+ }
60
+
61
+ OPEN_CASE_STATES = frozenset({CaseState.REVIEW_REQUESTED, CaseState.CHANGES_REQUESTED})
62
+
63
+
64
+ class CaseVersion(NamedTuple):
65
+ case_id: str
66
+ content_hash: str
67
+
68
+ @classmethod
69
+ def of(cls, case: CaseView) -> CaseVersion:
70
+ return cls(case.case_id, case.content_hash)
71
+
72
+
73
+ class NewCaseEvent(BaseModel):
74
+ dataset: str
75
+ case_id: str
76
+ content_hash: str
77
+ kind: CaseEventKind
78
+ actor: str
79
+ target: str | None = None
80
+
81
+
82
+ class CaseEvent(NewCaseEvent):
83
+ id: int
84
+ created_at: datetime
85
+
86
+
87
+ class NewCaseComment(BaseModel):
88
+ dataset: str
89
+ case_id: str
90
+ content_hash: str
91
+ parent_id: int | None = None
92
+ author: str
93
+ body: str
94
+
95
+
96
+ class CaseComment(NewCaseComment):
97
+ id: int
98
+ resolved: bool
99
+ created_at: datetime
100
+
101
+
102
+ @dataclass
103
+ class CaseReview:
104
+ dataset: str
105
+ case_id: str
106
+ content_hash: str
107
+ last_event: CaseEvent | None
108
+ requested_to: str | None
109
+
110
+ @classmethod
111
+ def unreviewed(cls, dataset: str, case_id: str, content_hash: str) -> CaseReview:
112
+ return cls(
113
+ dataset=dataset,
114
+ case_id=case_id,
115
+ content_hash=content_hash,
116
+ last_event=None,
117
+ requested_to=None,
118
+ )
119
+
120
+ @property
121
+ def version(self) -> CaseVersion:
122
+ return CaseVersion(self.case_id, self.content_hash)
123
+
124
+ def state(self) -> CaseState:
125
+ if self.last_event is None:
126
+ return CaseState.UNREVIEWED
127
+ return _CASE_STATE_FOR[self.last_event.kind]
128
+
129
+ def synced(self, actor: str) -> NewCaseEvent:
130
+ return self._event(CaseEventKind.SYNCED, actor)
131
+
132
+ def request_review(self, actor: str, target: str) -> NewCaseEvent:
133
+ return self._event(CaseEventKind.REVIEW_REQUESTED, actor, target)
134
+
135
+ def approve(self, actor: str) -> NewCaseEvent:
136
+ return self._event(CaseEventKind.APPROVED, actor)
137
+
138
+ def request_changes(self, actor: str) -> NewCaseEvent:
139
+ return self._event(CaseEventKind.CHANGES_REQUESTED, actor)
140
+
141
+ def carry_over(self, actor: str, new_hash: str) -> NewCaseEvent | None:
142
+ if self.last_event is None or self.state() not in OPEN_CASE_STATES:
143
+ return None
144
+ return NewCaseEvent(
145
+ dataset=self.dataset,
146
+ case_id=self.case_id,
147
+ content_hash=new_hash,
148
+ kind=CaseEventKind.REVIEW_REQUESTED,
149
+ actor=actor,
150
+ target=self.requested_to or self.last_event.actor,
151
+ )
152
+
153
+ def _event(self, kind: CaseEventKind, actor: str, target: str | None = None) -> NewCaseEvent:
154
+ return NewCaseEvent(
155
+ dataset=self.dataset,
156
+ case_id=self.case_id,
157
+ content_hash=self.content_hash,
158
+ kind=kind,
159
+ actor=actor,
160
+ target=target,
161
+ )
162
+
163
+
164
+ def review_summary(review: CaseReview) -> dict[str, Any]:
165
+ state = review.state()
166
+ return {
167
+ "state": state.value,
168
+ "requested_to": review.requested_to if state is CaseState.REVIEW_REQUESTED else None,
169
+ "updated_at": review.last_event.created_at if review.last_event is not None else None,
170
+ }
171
+
172
+
173
+ class ReviewLogProtocol(Protocol):
174
+ async def load(self, dataset: str, versions: list[CaseVersion]) -> dict[str, CaseReview]: ...
175
+
176
+ async def latest_hashes(self, dataset: str, case_ids: list[str]) -> dict[str, str]: ...
177
+
178
+ async def append(self, events: list[NewCaseEvent]) -> None: ...
179
+
180
+ async def decide(
181
+ self,
182
+ event: NewCaseEvent,
183
+ comment: NewCaseComment | None,
184
+ after: Callable[[Any, int], Awaitable[None]] | None = None,
185
+ ) -> None: ...
186
+
187
+ async def history(self, dataset: str, case_id: str) -> list[CaseEvent]: ...
188
+
189
+ async def comments(self, dataset: str, case_id: str) -> list[CaseComment]: ...
190
+
191
+ async def add_comment(self, comment: NewCaseComment) -> CaseComment: ...
192
+
193
+ async def get_comment(self, comment_id: int) -> CaseComment | None: ...
194
+
195
+ async def set_resolved(self, comment_id: int, resolved: bool) -> CaseComment: ...
196
+
197
+
198
+ def group_threads(comments: list[CaseComment]) -> list[dict[str, Any]]:
199
+ ordered = sorted(comments, key=lambda c: (c.created_at, c.id))
200
+ roots = [c.model_dump() | {"replies": []} for c in ordered if c.parent_id is None]
201
+ by_id = {root["id"]: root for root in roots}
202
+ for comment in ordered:
203
+ if comment.parent_id is not None:
204
+ by_id[comment.parent_id]["replies"].append(comment.model_dump())
205
+ return roots
206
+
207
+
208
+ class NotReviewable(Exception):
209
+ def __init__(self, dataset: str):
210
+ self.dataset = dataset
211
+ super().__init__(f"{dataset} is not a suite in a shared workspace; review does not apply")
212
+
213
+
214
+ def is_reviewable(dataset: DatasetRow) -> bool:
215
+ return dataset.kind == "suite" and dataset.workspace_is_shared
216
+
217
+
218
+ def ensure_reviewable(dataset: DatasetRow) -> None:
219
+ if not is_reviewable(dataset):
220
+ raise NotReviewable(dataset.ref)