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,616 @@
1
+ import asyncio
2
+ import logging
3
+ import uuid
4
+ from collections.abc import Coroutine
5
+ from typing import Any
6
+
7
+ import httpx
8
+ from fastapi import APIRouter, HTTPException, Query, Request
9
+ from pydantic import BaseModel
10
+ from sqlalchemy import select
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+ from starlette.responses import Response, StreamingResponse
13
+
14
+ from evalkit.api.access import ActorDep, ensure_active, require, workspace_ref
15
+ from evalkit.api.deps import (
16
+ AgentStoreDep,
17
+ HolderDep,
18
+ PgStoreDep,
19
+ RunnerDepsDep,
20
+ SessionDep,
21
+ )
22
+ from evalkit.api.llm_providers import NO_DEFAULT_JUDGE, require_registered_judge
23
+ from evalkit.api.server_keys import server_keys, with_keys
24
+ from evalkit.contracts.atif import validate_trajectory
25
+ from evalkit.contracts.enums import ExperimentStatus, SyncSource
26
+ from evalkit.core.access import (
27
+ Actor,
28
+ can_change_experiment,
29
+ can_create_experiment,
30
+ can_use_agent,
31
+ )
32
+ from evalkit.core.agents import AgentDefinition, explicit, server_may_drive
33
+ from evalkit.core.blob import trajectory_key
34
+ from evalkit.core.costmap import load_cost_map
35
+ from evalkit.core.runner import (
36
+ ACTIVE,
37
+ REFETCHABLE,
38
+ ExperimentRequest,
39
+ InvalidCaseInputs,
40
+ NothingRunnable,
41
+ RunnerDeps,
42
+ error_text,
43
+ harness_capabilities,
44
+ prepare_experiment,
45
+ rescore_experiment,
46
+ run_experiment,
47
+ stored_trajectory_doc,
48
+ )
49
+ from evalkit.core.scorecard import assemble_report
50
+ from evalkit.core.trajectory import backfill_step_costs
51
+ from evalkit.core.workspace import Reference, parse_slug
52
+ from evalkit.db import experiments as db_experiments
53
+ from evalkit.db import scorecard as db_scorecard
54
+ from evalkit.db.cases import dataset_by_ref
55
+ from evalkit.db.models import AgentRun as AgentRunRow
56
+ from evalkit.db.models import ExperimentRun as ExperimentRunRow
57
+ from evalkit.db.models import User
58
+ from evalkit.db.runs import record_run as db_record_run
59
+ from evalkit.store.base import (
60
+ AgentRunRecord,
61
+ EvaluationRow,
62
+ Experiment,
63
+ GitRevision,
64
+ InvalidRun,
65
+ )
66
+
67
+ LOGGER = logging.getLogger(__name__)
68
+
69
+ router = APIRouter(prefix="/api/experiments", tags=["experiments"])
70
+ runs_router = APIRouter(prefix="/api/experiment-runs", tags=["experiments"])
71
+
72
+ INTERRUPTED_ERROR = "interrupted: evalkit server shut down"
73
+
74
+ TaskRegistry = dict[str, tuple[int, str, asyncio.Task]]
75
+ LIST_LIMIT = 200
76
+
77
+
78
+ class ExperimentPatch(BaseModel):
79
+ status: ExperimentStatus | None = None
80
+ error: str | None = None
81
+ holder: str | None = None
82
+ model: str | None = None
83
+
84
+
85
+ class LeaseRequest(BaseModel):
86
+ holder: str
87
+
88
+
89
+ class RunPost(BaseModel):
90
+ case_id: str
91
+ repetition: int
92
+ run: AgentRunRecord
93
+ holder: str
94
+
95
+
96
+ class EvaluationsPost(BaseModel):
97
+ rows: list[EvaluationRow]
98
+ force: bool = False
99
+ holder: str
100
+
101
+
102
+ def _consume_exception(task: asyncio.Task) -> None:
103
+ def _cb(t: asyncio.Task) -> None:
104
+ if t.cancelled():
105
+ return
106
+ exc = t.exception()
107
+ if exc is not None:
108
+ LOGGER.error(
109
+ "experiment task %s failed: %s", t.get_name(), error_text(exc), exc_info=exc
110
+ )
111
+
112
+ task.add_done_callback(_cb)
113
+
114
+
115
+ def task_holder(pod: str) -> str:
116
+ return f"{pod}:{uuid.uuid4().hex[:12]}"
117
+
118
+
119
+ def _track(
120
+ request: Request, exp_id: int, holder: str, name: str, coro: Coroutine[Any, Any, None]
121
+ ) -> None:
122
+ task = asyncio.create_task(coro, name=name)
123
+ _consume_exception(task)
124
+ registry: TaskRegistry = request.app.state.tasks
125
+ registry[name] = (exp_id, holder, task)
126
+ task.add_done_callback(lambda t: registry.pop(name, None))
127
+
128
+
129
+ async def interrupt_tasks(store, registry: TaskRegistry) -> None:
130
+ tracked = list(registry.values())
131
+ for _, _, task in tracked:
132
+ task.cancel()
133
+ await asyncio.gather(*(task for _, _, task in tracked), return_exceptions=True)
134
+ results = await asyncio.gather(
135
+ *(
136
+ store.transition(exp_id, ExperimentStatus.FAILED, holder, error=INTERRUPTED_ERROR)
137
+ for exp_id, holder, task in tracked
138
+ if task.cancelled()
139
+ ),
140
+ return_exceptions=True,
141
+ )
142
+ for result in results:
143
+ if isinstance(result, BaseException):
144
+ LOGGER.warning("could not mark an interrupted experiment failed: %s", result)
145
+
146
+
147
+ def slug_or_400(value: str) -> Reference:
148
+ try:
149
+ return parse_slug(value)
150
+ except ValueError as exc:
151
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
152
+
153
+
154
+ async def _creator_id(session: AsyncSession, exp: Experiment) -> int | None:
155
+ return await session.scalar(select(User.id).where(User.username == exp.created_by))
156
+
157
+
158
+ async def may_change(session: AsyncSession, actor: Actor, exp: Experiment) -> bool:
159
+ ws = await workspace_ref(session, exp.workspace)
160
+ return can_change_experiment(actor, ws, await _creator_id(session, exp))
161
+
162
+
163
+ async def _changeable(session: AsyncSession, actor: Actor, exp_id: int) -> Experiment:
164
+ exp = await db_experiments.get_experiment(session, exp_id)
165
+ require(await may_change(session, actor, exp), "you cannot change this experiment")
166
+ return exp
167
+
168
+
169
+ async def _agent(deps: RunnerDeps, exp: Experiment) -> AgentDefinition:
170
+ return await deps.agents.get(explicit(parse_slug(exp.agent)))
171
+
172
+
173
+ def _local_hint(agent: AgentDefinition, exp: Experiment, command: str) -> str:
174
+ if command == "evaluate":
175
+ return f"evalkit evaluate {exp.id}"
176
+ if agent.harness_url is None:
177
+ return f"run_experiment(..., resume={exp.id}) from your script"
178
+ return f"evalkit run {exp.dataset} --agent {exp.agent} --resume {exp.id}"
179
+
180
+
181
+ async def _server_driven(deps: RunnerDeps, exp: Experiment, command: str) -> AgentDefinition:
182
+ agent = await _agent(deps, exp)
183
+ if not server_may_drive(agent):
184
+ raise HTTPException(
185
+ status_code=409, detail=f"{command} it with {_local_hint(agent, exp, command)}"
186
+ )
187
+ if agent.archived:
188
+ raise HTTPException(status_code=409, detail=f"agent {agent.ref} is archived")
189
+ return agent
190
+
191
+
192
+ async def _runner_written(deps: RunnerDeps, exp: Experiment) -> AgentDefinition:
193
+ agent = await _agent(deps, exp)
194
+ if server_may_drive(agent):
195
+ raise HTTPException(
196
+ status_code=409,
197
+ detail=f"experiment {exp.id} is driven by the server; only its runner writes to it",
198
+ )
199
+ return agent
200
+
201
+
202
+ async def _last_git_revision(session: AsyncSession, dataset: str) -> GitRevision | None:
203
+ row = await dataset_by_ref(session, dataset)
204
+ sync = row.last_sync
205
+ if sync is None or sync.source != SyncSource.GIT.value:
206
+ return None
207
+ return GitRevision(
208
+ sha=sync.detail.get("sha"), branch=sync.detail.get("branch"), dirty=sync.detail.get("dirty")
209
+ )
210
+
211
+
212
+ @router.post("")
213
+ async def create_experiment(
214
+ body: ExperimentRequest,
215
+ request: Request,
216
+ deps: RunnerDepsDep,
217
+ store: PgStoreDep,
218
+ agents: AgentStoreDep,
219
+ session: SessionDep,
220
+ actor: ActorDep,
221
+ pod: HolderDep,
222
+ ) -> dict:
223
+ agent = await agents.get(explicit(slug_or_400(body.agent)))
224
+ if agent.archived:
225
+ raise HTTPException(status_code=409, detail=f"agent {agent.ref} is archived")
226
+ agent_ws = await workspace_ref(session, agent.workspace)
227
+ require(
228
+ can_use_agent(actor, agent_ws),
229
+ f"only the owner of @{agent.workspace} can run {agent.ref}",
230
+ )
231
+ dataset_ws = await workspace_ref(
232
+ session, (await dataset_by_ref(session, body.dataset)).workspace
233
+ )
234
+ ensure_active(agent_ws)
235
+ ensure_active(dataset_ws)
236
+ if dataset_ws.is_shared and agent_ws.is_shared:
237
+ require(
238
+ can_create_experiment(actor, dataset_ws),
239
+ f"experiments of {body.dataset} with {agent.ref} belong to @{dataset_ws.name}; "
240
+ "only its writers, reviewers and admins may start them",
241
+ )
242
+ server_driven = server_may_drive(agent)
243
+ if server_driven and body.capabilities is not None:
244
+ raise HTTPException(
245
+ status_code=400, detail="capabilities are fetched by the server for shared agents"
246
+ )
247
+ if not server_driven and (body.capabilities is None or body.holder is None):
248
+ raise HTTPException(
249
+ status_code=400,
250
+ detail="capabilities and holder are required for agents the server does not drive",
251
+ )
252
+ providers = request.app.state.llm_providers
253
+ judge = body.judge or await providers.default_judge()
254
+ if judge is None:
255
+ raise HTTPException(status_code=400, detail=f"{NO_DEFAULT_JUDGE}, or pass a judge")
256
+ api_key: str | None = None
257
+ warnings: list[str] = []
258
+ run_deps = deps
259
+ if server_driven:
260
+ await require_registered_judge(providers, judge.base_url)
261
+ keys, warnings = await server_keys(request, agent, judge.base_url)
262
+ api_key = keys.key_for("agent", agent.ref)
263
+ run_deps = with_keys(deps, keys)
264
+ holder = task_holder(pod) if server_driven else str(body.holder)
265
+ try:
266
+ caps = body.capabilities or await harness_capabilities(
267
+ agent, api_key, deps.harness_transport
268
+ )
269
+ exp, dropped = await prepare_experiment(
270
+ store,
271
+ body,
272
+ agent=agent,
273
+ caps=caps,
274
+ remote_evaluators=await deps.evaluators.in_workspace(agent.workspace),
275
+ judge=judge,
276
+ runner=actor.username,
277
+ holder=holder,
278
+ revision=None
279
+ if body.cases is not None
280
+ else await _last_git_revision(session, body.dataset),
281
+ )
282
+ except (InvalidCaseInputs, NothingRunnable) as exc:
283
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
284
+ except httpx.HTTPError as exc:
285
+ raise HTTPException(status_code=502, detail=f"could not start experiment: {exc}") from exc
286
+ if server_driven:
287
+ _track(request, exp.id, holder, f"run:{exp.id}", run_experiment(run_deps, exp.id, holder))
288
+ return {**exp.model_dump(mode="json"), "dropped_cases": dropped, "warnings": warnings}
289
+
290
+
291
+ async def with_progress(session: AsyncSession, experiments: list[Experiment]) -> list[dict]:
292
+ ids = [e.id for e in experiments]
293
+ scores = await db_scorecard.list_scores(session, ids)
294
+ costs = await db_scorecard.list_costs(session, ids)
295
+ progress = await db_scorecard.progress_many(session, ids)
296
+ return [
297
+ {
298
+ **exp.model_dump(mode="json"),
299
+ "progress": progress[exp.id].model_dump(),
300
+ "scores": scores.get(exp.id, {}),
301
+ **costs.get(exp.id, {}),
302
+ }
303
+ for exp in experiments
304
+ ]
305
+
306
+
307
+ @router.get("")
308
+ async def list_experiments(
309
+ session: SessionDep,
310
+ workspace: str | None = None,
311
+ status: ExperimentStatus | None = None,
312
+ limit: int = Query(50, ge=1),
313
+ offset: int = Query(0, ge=0),
314
+ brief: bool = False,
315
+ ) -> list[dict]:
316
+ workspace_id = (await workspace_ref(session, workspace)).id if workspace else None
317
+ found = await db_experiments.list_experiments(
318
+ session,
319
+ workspace_id=workspace_id,
320
+ status=status,
321
+ limit=min(limit, LIST_LIMIT),
322
+ offset=offset,
323
+ )
324
+ if brief:
325
+ return [exp.model_dump(mode="json", exclude={"config"}) for exp in found]
326
+ return await with_progress(session, found)
327
+
328
+
329
+ @router.get("/compare")
330
+ async def compare(base: int, compare: int, store: PgStoreDep) -> dict:
331
+ base_rows = await store.aggregate(base)
332
+ compare_rows = await store.aggregate(compare)
333
+ return {
334
+ "base": assemble_report(base_rows).model_dump(),
335
+ "compare": assemble_report(compare_rows, baseline=base_rows).model_dump(),
336
+ }
337
+
338
+
339
+ @router.get("/{exp_id}")
340
+ async def get_experiment(
341
+ exp_id: int, deps: RunnerDepsDep, session: SessionDep, actor: ActorDep
342
+ ) -> dict:
343
+ exp = await db_experiments.get_experiment(session, exp_id)
344
+ pending_runs = await db_experiments.has_pending_runs(session, exp_id)
345
+ return {
346
+ **exp.model_dump(mode="json"),
347
+ "progress": (await db_scorecard.progress(session, exp_id)).model_dump(),
348
+ "case_states": await db_scorecard.case_states(session, exp_id),
349
+ "can_change": await may_change(session, actor, exp),
350
+ "server_may_drive": server_may_drive(await _agent(deps, exp)),
351
+ "pending_runs": pending_runs,
352
+ "resumable": db_experiments.resumable(exp, pending_runs),
353
+ }
354
+
355
+
356
+ @router.get("/{exp_id}/cases")
357
+ async def pinned_cases(exp_id: int, store: PgStoreDep) -> list[dict]:
358
+ return [case.model_dump(mode="json") for case in await store.pinned_cases(exp_id)]
359
+
360
+
361
+ @router.get("/{exp_id}/runs")
362
+ async def list_runs(exp_id: int, deps: RunnerDepsDep, store: PgStoreDep) -> list[dict]:
363
+ exp = await store.get_experiment(exp_id)
364
+ rows = []
365
+ for run in await store.run_details(exp_id):
366
+ if run.agent_run.trajectory_status in REFETCHABLE:
367
+ _, status = await stored_trajectory_doc(deps.blob_store, exp.agent_id, run.agent_run)
368
+ run.agent_run.trajectory_status = status
369
+ rows.append(run.model_dump(mode="json"))
370
+ return rows
371
+
372
+
373
+ @router.post("/{exp_id}/runs")
374
+ async def record_run(
375
+ exp_id: int, body: RunPost, deps: RunnerDepsDep, session: SessionDep, actor: ActorDep
376
+ ) -> dict:
377
+ exp = await _changeable(session, actor, exp_id)
378
+ await _runner_written(deps, exp)
379
+ expected = trajectory_key(exp.agent_id, body.run.harness_run_id)
380
+ if body.run.trajectory_key is not None and body.run.trajectory_key != expected:
381
+ raise HTTPException(status_code=400, detail=f"trajectory_key must be {expected}")
382
+ try:
383
+ ref = await db_record_run(
384
+ session,
385
+ exp_id,
386
+ body.case_id,
387
+ body.repetition,
388
+ body.run,
389
+ actor.id,
390
+ holder=body.holder,
391
+ )
392
+ except InvalidRun as exc:
393
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
394
+ await session.commit()
395
+ return ref.model_dump()
396
+
397
+
398
+ @router.get("/{exp_id}/pending")
399
+ async def pending_runs(exp_id: int, store: PgStoreDep) -> list[dict]:
400
+ return [rep.model_dump() for rep in await store.pending_runs(exp_id)]
401
+
402
+
403
+ @router.get("/{exp_id}/harness-run-id")
404
+ async def harness_run_id_used(exp_id: int, value: str, store: PgStoreDep) -> dict:
405
+ return {"used": await store.harness_run_id_used(exp_id, value)}
406
+
407
+
408
+ @router.get("/{exp_id}/runs-to-score")
409
+ async def runs_to_score(exp_id: int, store: PgStoreDep, force: bool = False) -> list[dict]:
410
+ return [run.model_dump(mode="json") for run in await store.runs_to_score(exp_id, force=force)]
411
+
412
+
413
+ @runs_router.post("/{run_id}/evaluations")
414
+ async def publish_evaluations(
415
+ run_id: int,
416
+ body: EvaluationsPost,
417
+ deps: RunnerDepsDep,
418
+ store: PgStoreDep,
419
+ session: SessionDep,
420
+ actor: ActorDep,
421
+ ) -> dict:
422
+ exp_id = await db_experiments.experiment_of_run(session, run_id)
423
+ if exp_id is None:
424
+ raise HTTPException(status_code=404, detail=f"unknown experiment run: {run_id}")
425
+ await _runner_written(deps, await _changeable(session, actor, exp_id))
426
+ await store.publish_evaluations(run_id, body.rows, force=body.force, holder=body.holder)
427
+ return {"ok": True}
428
+
429
+
430
+ @router.patch("/{exp_id}")
431
+ async def patch_experiment(
432
+ exp_id: int,
433
+ body: ExperimentPatch,
434
+ deps: RunnerDepsDep,
435
+ store: PgStoreDep,
436
+ session: SessionDep,
437
+ actor: ActorDep,
438
+ ) -> dict:
439
+ agent = await _runner_written(deps, await _changeable(session, actor, exp_id))
440
+ if body.status in ACTIVE and agent.archived:
441
+ raise HTTPException(status_code=409, detail=f"agent {agent.ref} is archived")
442
+ if body.model is not None:
443
+ await store.set_model(exp_id, body.model)
444
+ if body.status is not None:
445
+ await store.transition(exp_id, body.status, body.holder, error=body.error)
446
+ return (await store.get_experiment(exp_id)).model_dump(mode="json")
447
+
448
+
449
+ @router.post("/{exp_id}/lease")
450
+ async def renew_lease(
451
+ exp_id: int, body: LeaseRequest, store: PgStoreDep, session: SessionDep, actor: ActorDep
452
+ ) -> dict:
453
+ await _changeable(session, actor, exp_id)
454
+ return {"renewed": await store.renew_lease(exp_id, body.holder)}
455
+
456
+
457
+ @router.get("/{exp_id}/scorecard")
458
+ async def scorecard(exp_id: int, store: PgStoreDep, baseline_id: int | None = None) -> dict:
459
+ baseline = await store.aggregate(baseline_id) if baseline_id is not None else None
460
+ return assemble_report(await store.aggregate(exp_id), baseline=baseline).model_dump()
461
+
462
+
463
+ @router.post("/{exp_id}/evaluate")
464
+ async def evaluate(
465
+ exp_id: int,
466
+ request: Request,
467
+ deps: RunnerDepsDep,
468
+ store: PgStoreDep,
469
+ session: SessionDep,
470
+ actor: ActorDep,
471
+ pod: HolderDep,
472
+ ) -> dict:
473
+ exp = await _changeable(session, actor, exp_id)
474
+ agent = await _server_driven(deps, exp, "evaluate")
475
+ await require_registered_judge(request.app.state.llm_providers, exp.config.judge.base_url)
476
+ keys, warnings = await server_keys(request, agent, exp.config.judge.base_url)
477
+ holder = task_holder(pod)
478
+ exp = await store.transition(exp_id, ExperimentStatus.EVALUATING, holder)
479
+ _track(
480
+ request,
481
+ exp_id,
482
+ holder,
483
+ f"evaluate:{exp_id}",
484
+ rescore_experiment(with_keys(deps, keys), exp_id, holder),
485
+ )
486
+ return {**exp.model_dump(mode="json"), "warnings": warnings}
487
+
488
+
489
+ @router.post("/{exp_id}/resume")
490
+ async def resume(
491
+ exp_id: int,
492
+ request: Request,
493
+ deps: RunnerDepsDep,
494
+ store: PgStoreDep,
495
+ session: SessionDep,
496
+ actor: ActorDep,
497
+ pod: HolderDep,
498
+ ) -> dict:
499
+ exp = await _changeable(session, actor, exp_id)
500
+ agent = await _server_driven(deps, exp, "resume")
501
+ await require_registered_judge(request.app.state.llm_providers, exp.config.judge.base_url)
502
+ keys, warnings = await server_keys(request, agent, exp.config.judge.base_url)
503
+ holder = task_holder(pod)
504
+ exp = await store.transition(exp_id, ExperimentStatus.RUNNING, holder)
505
+ _track(
506
+ request,
507
+ exp_id,
508
+ holder,
509
+ f"run:{exp_id}",
510
+ run_experiment(with_keys(deps, keys), exp_id, holder),
511
+ )
512
+ return {**exp.model_dump(mode="json"), "warnings": warnings}
513
+
514
+
515
+ @router.get("/{exp_id}/runs/{run_id}/trajectory")
516
+ async def stored_trajectory(
517
+ exp_id: int, run_id: int, deps: RunnerDepsDep, store: PgStoreDep
518
+ ) -> dict:
519
+ exp = await store.get_experiment(exp_id)
520
+ run = next((r for r in await store.run_details(exp_id) if r.experiment_run_id == run_id), None)
521
+ if run is None:
522
+ raise HTTPException(status_code=404, detail="unknown run")
523
+ doc, _ = await stored_trajectory_doc(deps.blob_store, exp.agent_id, run.agent_run)
524
+ if doc is None:
525
+ raise HTTPException(status_code=404, detail="run has no stored trajectory")
526
+ trajectory = validate_trajectory(doc)
527
+ # No-op for runs stored after per-step pricing landed; prices older
528
+ # trajectories so the cost view matches the scorecard gauge.
529
+ backfill_step_costs(trajectory, load_cost_map(deps.cfg.cost_map))
530
+ return trajectory.model_dump(exclude_none=True)
531
+
532
+
533
+ def _harness_run_id(exp: Experiment, key: str) -> str | None:
534
+ for folder in ("trajectories", "workspaces"):
535
+ prefix = f"{folder}/{exp.agent_id}/"
536
+ if key.startswith(prefix) and key.endswith(".json"):
537
+ return key.removeprefix(prefix).removesuffix(".json") or None
538
+ run_id, _, path = key.removeprefix(f"artifacts/{exp.agent_id}/").partition("/")
539
+ return run_id if run_id and path else None
540
+
541
+
542
+ def _check_blob_key(exp: Experiment, key: str) -> None:
543
+ folders = ("trajectories", "artifacts", "workspaces")
544
+ if not key.startswith(tuple(f"{folder}/{exp.agent_id}/" for folder in folders)):
545
+ raise HTTPException(
546
+ status_code=400,
547
+ detail="key must be under this experiment's agent's "
548
+ "trajectories/, artifacts/ or workspaces/",
549
+ )
550
+
551
+
552
+ @router.get("/{exp_id}/blob")
553
+ async def list_blob(exp_id: int, prefix: str, deps: RunnerDepsDep, store: PgStoreDep) -> dict:
554
+ _check_blob_key(await store.get_experiment(exp_id), prefix)
555
+ return {"objects": await deps.blob_store.list_objects(prefix)}
556
+
557
+
558
+ @router.put("/{exp_id}/blob/{key:path}")
559
+ async def put_blob(
560
+ exp_id: int,
561
+ key: str,
562
+ request: Request,
563
+ deps: RunnerDepsDep,
564
+ session: SessionDep,
565
+ actor: ActorDep,
566
+ ) -> dict:
567
+ exp = await _changeable(session, actor, exp_id)
568
+ await _runner_written(deps, exp)
569
+ _check_blob_key(exp, key)
570
+ harness_run_id = _harness_run_id(exp, key)
571
+ if harness_run_id is None:
572
+ raise HTTPException(status_code=400, detail=f"{key} names no run")
573
+ recorded = (
574
+ await session.execute(
575
+ select(AgentRunRow.id, ExperimentRunRow.experiment_id, AgentRunRow.trajectory_key)
576
+ .outerjoin(ExperimentRunRow, ExperimentRunRow.agent_run_id == AgentRunRow.id)
577
+ .where(
578
+ AgentRunRow.agent_id == exp.agent_id, AgentRunRow.harness_run_id == harness_run_id
579
+ )
580
+ )
581
+ ).one_or_none()
582
+ if recorded is not None and recorded[1] != exp_id:
583
+ raise HTTPException(
584
+ status_code=403, detail=f"run {harness_run_id} does not belong to experiment {exp_id}"
585
+ )
586
+ if recorded is not None and recorded[2] == key and exp.status is ExperimentStatus.RUNNING:
587
+ raise HTTPException(
588
+ status_code=409,
589
+ detail=f"session id {harness_run_id} was already used in experiment {exp_id}; "
590
+ "each run needs its own session id",
591
+ )
592
+ await deps.blob_store.put_stream(key, request.stream())
593
+ return {"ok": True}
594
+
595
+
596
+ async def _existing_blob_key(exp_id: int, key: str, deps: RunnerDeps, store) -> None:
597
+ _check_blob_key(await store.get_experiment(exp_id), key)
598
+ if not await deps.blob_store.object_exists(key):
599
+ raise HTTPException(status_code=404, detail="no such object")
600
+
601
+
602
+ @router.head("/{exp_id}/blob/{key:path}")
603
+ async def head_blob(exp_id: int, key: str, deps: RunnerDepsDep, store: PgStoreDep) -> Response:
604
+ await _existing_blob_key(exp_id, key, deps, store)
605
+ return Response(status_code=200)
606
+
607
+
608
+ @router.get("/{exp_id}/blob/{key:path}")
609
+ async def get_blob(exp_id: int, key: str, deps: RunnerDepsDep, store: PgStoreDep) -> Response:
610
+ await _existing_blob_key(exp_id, key, deps, store)
611
+ blob = await deps.blob_store.get_stream(key)
612
+ return StreamingResponse(
613
+ blob.chunks,
614
+ media_type="application/octet-stream",
615
+ headers={"Content-Length": str(blob.size)},
616
+ )