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
evalkit/sdk/runner.py ADDED
@@ -0,0 +1,957 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import sys
6
+ import time
7
+ import uuid
8
+ from collections.abc import Callable, Sequence
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import httpx
14
+ from opentelemetry import trace
15
+ from opentelemetry.context import Context
16
+ from pydantic import BaseModel
17
+
18
+ from evalkit import version
19
+ from evalkit.client import connection
20
+ from evalkit.client.agents import HttpAgents
21
+ from evalkit.client.api import ApiRequestError, AsyncApi, SyncApi, encode
22
+ from evalkit.client.blob import HttpBlobStore
23
+ from evalkit.client.evaluators import HttpEvaluators
24
+ from evalkit.client.sessions import HttpSessionReader
25
+ from evalkit.client.store import HttpStore, local_holder
26
+ from evalkit.client.summary import summary_lines
27
+ from evalkit.client.sync import git_revision, resolve_dataset, sync_dataset
28
+ from evalkit.config import LocalConfig
29
+ from evalkit.contracts.atif import TrajectoryValidationError, validate_trajectory
30
+ from evalkit.contracts.enums import ExperimentStatus, RunStatus, TrajectoryStatus
31
+ from evalkit.contracts.settings import JudgeSettings
32
+ from evalkit.core.agents import AgentDefinition, explicit
33
+ from evalkit.core.blob import trajectory_key
34
+ from evalkit.core.capabilities import sdk_capabilities
35
+ from evalkit.core.costmap import load_cost_map
36
+ from evalkit.core.python_eval import PythonEvaluator
37
+ from evalkit.core.runner import (
38
+ ExperimentRequest,
39
+ LeaseLost,
40
+ RunnerDeps,
41
+ ScoringFailed,
42
+ case_title,
43
+ dropped_cases_text,
44
+ evaluate_experiment,
45
+ judge_key,
46
+ judge_key_for,
47
+ missing_local_keys,
48
+ rescore_experiment,
49
+ run_leased,
50
+ )
51
+ from evalkit.core.traces.mapping import SDK_ROOT_ATTRIBUTE, TASK_SESSION_ATTRIBUTE
52
+ from evalkit.core.traces.poll import poll_trajectory
53
+ from evalkit.core.traces.sessions import SessionReader
54
+ from evalkit.core.trajectory import TrajectoryView, backfill_step_costs, derive_metrics
55
+ from evalkit.core.workspace import Reference, parse_reference, parse_slug, slug
56
+ from evalkit.credentials import CredentialRegistry
57
+ from evalkit.sdk.binding import (
58
+ Fixture,
59
+ TaskSpec,
60
+ arguments,
61
+ call_task,
62
+ ignores_model,
63
+ inspect_task,
64
+ )
65
+ from evalkit.sdk.tracing import CURRENT_RUN
66
+ from evalkit.store.base import AgentRunRecord, CaseView, Experiment, Selection
67
+
68
+ ROOT_SPAN_NAME = "evalkit.run"
69
+ NO_PROVIDER_ERROR = "no tracer provider configured and the task returned no session_id"
70
+ Task = Callable[..., Any]
71
+ Progress = Callable[[str], None]
72
+
73
+
74
+ class RunResult(BaseModel):
75
+ """What a task can return instead of a plain answer.
76
+
77
+ Attributes:
78
+ answer: The agent's answer text.
79
+ output: The agent's structured output.
80
+ model: The model the agent used, recorded on the run.
81
+ error: An error message; the run is recorded as failed.
82
+ session_id: The agent's own session id, when its spans do not continue the
83
+ SDK's trace. The SDK reads the trajectory from that session.
84
+ trajectory: An ATIF document to store as the trajectory, instead of building
85
+ one from spans.
86
+ """
87
+
88
+ answer: str | None = None
89
+ output: dict[str, Any] | None = None
90
+ model: str | None = None
91
+ error: str | None = None
92
+ session_id: str | None = None
93
+ trajectory: dict[str, Any] | None = None
94
+
95
+
96
+ class RunSummary(BaseModel):
97
+ """One run of an experiment.
98
+
99
+ Attributes:
100
+ case_id: The case.
101
+ repetition: 1 for the first repetition, 2 for the second, and so on.
102
+ status: `completed`, `failed`, `cancelled` or `timeout`.
103
+ trajectory_status: `valid`, `partial`, `invalid` or `unavailable`, or `None`
104
+ when no trajectory was read.
105
+ session_id: The session the run's spans belong to.
106
+ """
107
+
108
+ case_id: str
109
+ repetition: int
110
+ status: str
111
+ trajectory_status: str | None
112
+ session_id: str | None
113
+
114
+
115
+ class ExperimentResult(BaseModel):
116
+ """The result of `run_experiment` or `evaluate_experiment`.
117
+
118
+ Attributes:
119
+ experiment_id: The experiment, or `None` after a dry run.
120
+ url: The experiment's page in the web UI, or `None` after a dry run.
121
+ runs: One entry per case and repetition.
122
+ """
123
+
124
+ experiment_id: int | None
125
+ url: str | None
126
+ runs: list[RunSummary]
127
+
128
+
129
+ class Outcome(BaseModel):
130
+ run_id: str
131
+ started: datetime
132
+ answer: str | None = None
133
+ output: dict[str, Any] | None = None
134
+ model: str | None = None
135
+ error: str | None = None
136
+ status: str = "failed"
137
+ session_id: str | None = None
138
+ trajectory: dict[str, Any] | None = None
139
+ wall_ms: int = 0
140
+
141
+
142
+ def _config_search_starts() -> list[Path]:
143
+ script = getattr(sys.modules["__main__"], "__file__", None)
144
+ starts = [Path.cwd()]
145
+ if script is not None and Path(script).parent.resolve() != Path.cwd().resolve():
146
+ starts.append(Path(script).parent)
147
+ return starts
148
+
149
+
150
+ def _local_config() -> LocalConfig:
151
+ starts = _config_search_starts()
152
+ for start in starts:
153
+ local = LocalConfig.find(start)
154
+ if local is not None:
155
+ return local
156
+ raise FileNotFoundError(
157
+ f"evalkit.yaml not found above {' or '.join(str(s) for s in starts)}; "
158
+ "it holds your cases, fixtures and credentials"
159
+ )
160
+
161
+
162
+ async def _agent_definition(api: AsyncApi, agent: str, me: str) -> AgentDefinition:
163
+ definition = await HttpAgents(api).get(explicit(parse_reference(agent, me)))
164
+ if definition.harness_url is not None:
165
+ raise ValueError(
166
+ f"{definition.ref} has a harness_url, so evalkit drives it through its Harness API: "
167
+ f"run it with `evalkit run <dataset> --agent {agent}`"
168
+ )
169
+ return definition
170
+
171
+
172
+ def _credentials(local: LocalConfig, me: str) -> CredentialRegistry:
173
+ return CredentialRegistry(local.credentials, local.environ(), home=me)
174
+
175
+
176
+ async def _check_evaluator_names(
177
+ evaluators: Sequence[PythonEvaluator], api: AsyncApi, workspace: str
178
+ ) -> None:
179
+ remote = await HttpEvaluators(api).in_workspace(workspace)
180
+ clashes = sorted({e.name for e in evaluators} & set(remote))
181
+ if clashes:
182
+ raise ValueError(
183
+ f"python evaluator(s) {', '.join(clashes)} are also remote evaluators in @{workspace}"
184
+ )
185
+
186
+
187
+ def _coerce(result: Any) -> RunResult:
188
+ if isinstance(result, RunResult):
189
+ return result
190
+ if isinstance(result, str):
191
+ return RunResult(answer=result)
192
+ if isinstance(result, dict):
193
+ return RunResult(output=result)
194
+ raise TypeError(f"task returned {type(result).__name__}; return str, dict or RunResult")
195
+
196
+
197
+ def _root_attributes(
198
+ run_id: str, exp_id: int | None, case: CaseView, repetition: int
199
+ ) -> dict[str, Any]:
200
+ attributes: dict[str, Any] = {
201
+ "session.id": run_id,
202
+ "evalkit.case_id": case.case_id,
203
+ "evalkit.repetition": repetition,
204
+ }
205
+ if exp_id is not None:
206
+ attributes["evalkit.experiment_id"] = exp_id
207
+ return attributes
208
+
209
+
210
+ async def _fixtures(blob: HttpBlobStore, case: CaseView) -> list[Fixture]:
211
+ return [
212
+ Fixture(name=file.key, path=await blob.fixture_path(file), context=file.context)
213
+ for file in case.files
214
+ ]
215
+
216
+
217
+ async def _flush(provider: Any) -> None:
218
+ flush = getattr(provider, "force_flush", None)
219
+ if flush is not None:
220
+ await asyncio.to_thread(flush)
221
+
222
+
223
+ async def call_task_traced(
224
+ task: Task,
225
+ spec: TaskSpec,
226
+ *,
227
+ blob: HttpBlobStore,
228
+ case: CaseView,
229
+ repetition: int,
230
+ exp_id: int | None,
231
+ model: str | None,
232
+ provider: Any,
233
+ ) -> Outcome:
234
+ run_id = str(uuid.uuid4())
235
+ clock = time.monotonic()
236
+ metadata = {
237
+ "session_id": run_id,
238
+ "experiment_id": exp_id,
239
+ "case_id": case.case_id,
240
+ "repetition": repetition,
241
+ "tags": case.split_tags,
242
+ }
243
+ files = await _fixtures(blob, case)
244
+ args, kwargs = arguments(spec, input=case.input, files=files, model=model, metadata=metadata)
245
+ attributes = _root_attributes(run_id, exp_id, case, repetition)
246
+ tracer = provider.get_tracer("evalkit.sdk")
247
+ outcome = Outcome(run_id=run_id, started=datetime.now(UTC))
248
+ token = CURRENT_RUN.set(attributes)
249
+ recording = False
250
+ try:
251
+ with tracer.start_as_current_span(ROOT_SPAN_NAME, context=Context()) as span:
252
+ recording = span.is_recording()
253
+ if recording:
254
+ span.set_attributes(
255
+ {
256
+ **attributes,
257
+ SDK_ROOT_ATTRIBUTE: True,
258
+ "openinference.span.kind": "CHAIN",
259
+ "input.value": json.dumps(case.input),
260
+ }
261
+ )
262
+ try:
263
+ result = _coerce(await call_task(task, args, kwargs))
264
+ except Exception as exc:
265
+ if isinstance(exc, TypeError) and str(exc).startswith("task returned"):
266
+ outcome.error = f"task protocol violation: {exc}"
267
+ else:
268
+ text = str(exc)
269
+ outcome.error = f"{type(exc).__name__}: {text}" if text else type(exc).__name__
270
+ else:
271
+ _apply(outcome, result)
272
+ if recording and outcome.session_id is not None:
273
+ span.set_attributes(
274
+ {"session.id": str(outcome.session_id), TASK_SESSION_ATTRIBUTE: True}
275
+ )
276
+ if recording and (outcome.answer is not None or outcome.output is not None):
277
+ span.set_attribute(
278
+ "output.value",
279
+ outcome.answer
280
+ if outcome.answer is not None
281
+ else json.dumps(outcome.output),
282
+ )
283
+ finally:
284
+ CURRENT_RUN.reset(token)
285
+ outcome.wall_ms = int((time.monotonic() - clock) * 1000)
286
+ await _flush(provider)
287
+ if outcome.session_id is None and recording:
288
+ outcome.session_id = run_id
289
+ if outcome.session_id is None and outcome.trajectory is None and outcome.error is None:
290
+ outcome.error = NO_PROVIDER_ERROR
291
+ return outcome
292
+
293
+
294
+ def _apply(outcome: Outcome, result: RunResult) -> None:
295
+ if result.answer is None and result.output is None and result.error is None:
296
+ outcome.error = (
297
+ "task protocol violation: RunResult carries neither answer, output nor error"
298
+ )
299
+ return
300
+ outcome.answer = result.answer
301
+ outcome.output = result.output
302
+ outcome.model = result.model
303
+ outcome.error = result.error
304
+ has_result = result.answer is not None or result.output is not None
305
+ outcome.status = "completed" if has_result else "failed"
306
+ outcome.session_id = result.session_id
307
+ outcome.trajectory = result.trajectory
308
+
309
+
310
+ async def settle_and_record(
311
+ deps: RunnerDeps,
312
+ exp: Experiment,
313
+ case: CaseView,
314
+ repetition: int,
315
+ outcome: Outcome,
316
+ *,
317
+ wait_seconds: int,
318
+ model: str | None,
319
+ poll_interval: float,
320
+ holder: str | None = None,
321
+ used_sessions: set[str] | None = None,
322
+ ) -> RunSummary:
323
+ if used_sessions is not None and outcome.session_id is not None:
324
+ if outcome.session_id in used_sessions:
325
+ outcome.error = (
326
+ f"session id {outcome.session_id} was already used in experiment {exp.id}; "
327
+ "each run needs its own session id"
328
+ )
329
+ outcome.status = "failed"
330
+ outcome.session_id = None
331
+ outcome.trajectory = None
332
+ else:
333
+ used_sessions.add(outcome.session_id)
334
+ if outcome.session_id is not None:
335
+ used = await deps.store.harness_run_id_used(exp.id, outcome.session_id)
336
+ if used is not None:
337
+ outcome.error = used
338
+ outcome.status = "failed"
339
+ outcome.session_id = None
340
+ outcome.trajectory = None
341
+ trajectory_doc: dict[str, Any] | None = None
342
+ status: str | None
343
+ if outcome.trajectory is not None:
344
+ try:
345
+ validate_trajectory(outcome.trajectory)
346
+ trajectory_doc, status = outcome.trajectory, "valid"
347
+ except TrajectoryValidationError as exc:
348
+ status = "invalid"
349
+ deps.progress(f"run {case.case_id} #{repetition}: pushed trajectory invalid: {exc}")
350
+ elif outcome.session_id is None:
351
+ status = "unavailable"
352
+ else:
353
+ assert deps.sessions is not None
354
+ trajectory_doc, status = await poll_trajectory(
355
+ deps.sessions,
356
+ exp.agent,
357
+ outcome.session_id,
358
+ wait_seconds,
359
+ poll_interval=poll_interval,
360
+ )
361
+ key: str | None = None
362
+ metrics: dict[str, Any] = {}
363
+ if trajectory_doc is not None:
364
+ trajectory = validate_trajectory(trajectory_doc)
365
+ backfill_step_costs(trajectory, load_cost_map(deps.cfg.cost_map), model or outcome.model)
366
+ if status == "partial":
367
+ trajectory.extra = {**(trajectory.extra or {}), "trajectory_status": status}
368
+ key = trajectory_key(exp.agent_id, outcome.session_id or outcome.run_id)
369
+ await deps.blob_store.put_object(
370
+ key, json.dumps(trajectory.model_dump(exclude_none=True)).encode()
371
+ )
372
+ metrics = derive_metrics(TrajectoryView(trajectory), outcome.wall_ms).model_dump(
373
+ exclude_none=True
374
+ )
375
+ metrics.setdefault("duration_ms", outcome.wall_ms)
376
+ await deps.store.record_run(
377
+ exp.id,
378
+ case.case_id,
379
+ repetition,
380
+ AgentRunRecord(
381
+ harness_run_id=outcome.session_id or outcome.run_id,
382
+ status=RunStatus(outcome.status),
383
+ answer=outcome.answer,
384
+ output=outcome.output,
385
+ error=outcome.error,
386
+ started_at=outcome.started,
387
+ ended_at=datetime.now(UTC),
388
+ trajectory_key=key,
389
+ trajectory_status=TrajectoryStatus(status) if status else None,
390
+ metrics=metrics,
391
+ title=case_title(case.input),
392
+ ),
393
+ holder=holder,
394
+ )
395
+ deps.progress(f"run {case.case_id} #{repetition} {outcome.status} (trajectory {status})")
396
+ return RunSummary(
397
+ case_id=case.case_id,
398
+ repetition=repetition,
399
+ status=outcome.status,
400
+ trajectory_status=status,
401
+ session_id=outcome.session_id,
402
+ )
403
+
404
+
405
+ def _selection(case_ids: Sequence[str] | None, split: str | None) -> Selection:
406
+ if case_ids:
407
+ return Selection(case_ids=list(case_ids))
408
+ if split:
409
+ return Selection(tag=split)
410
+ return Selection(all=True)
411
+
412
+
413
+ async def _username(api: AsyncApi, conn: connection.Connection, progress: Progress) -> str:
414
+ me = (await api.get("/api/auth/me")).json()
415
+ warning = connection.token_expiry_warning(conn, me.get("token_expires_at"), datetime.now(UTC))
416
+ if warning is not None:
417
+ progress(f"warning: {warning}")
418
+ return str(me["username"])
419
+
420
+
421
+ def _note_how_to_finish(
422
+ exc: Exception, exp_id: int, python_evaluators: Sequence[str], progress: Progress
423
+ ) -> None:
424
+ call = f"evalkit.sdk.evaluate_experiment(experiment_id={exp_id}"
425
+ if python_evaluators:
426
+ how = f"{call}, evaluators=[...]) passing {', '.join(python_evaluators)}"
427
+ else:
428
+ how = f"{call}) or `evalkit evaluate {exp_id}`"
429
+ note = f"experiment {exp_id} is not fully scored; finish it with {how}"
430
+ exc.add_note(note)
431
+ progress(note)
432
+
433
+
434
+ def _experiment_url(server_url: str, exp_id: int) -> str:
435
+ return f"{server_url.rstrip('/')}/experiments/{exp_id}"
436
+
437
+
438
+ async def _session_summary(api: AsyncApi, path: str, session_id: str) -> str:
439
+ try:
440
+ session = (await api.get(path, params={"esf": "true"})).json()
441
+ except ApiRequestError as exc:
442
+ if exc.status != 413:
443
+ raise
444
+ return f" session {session_id}: {exc.detail}"
445
+ kinds: dict[str, int] = {}
446
+ for esf_span in session.get("esf") or []:
447
+ kinds[esf_span["kind"]] = kinds.get(esf_span["kind"], 0) + 1
448
+ version = session.get("mapping_version")
449
+ return (
450
+ f" session {session_id}: {session['span_count']} spans "
451
+ f"({', '.join(f'{k} {n}' for k, n in sorted(kinds.items())) or 'none'}), "
452
+ f"mapping {f'v{version}' if version else 'built-in'}, "
453
+ f"unmapped {', '.join(session.get('unmapped_attributes') or []) or 'none'}"
454
+ )
455
+
456
+
457
+ async def _report_session(
458
+ api: AsyncApi,
459
+ sessions: SessionReader,
460
+ agent_slug: str,
461
+ session_id: str,
462
+ *,
463
+ wait: int,
464
+ progress: Progress,
465
+ poll_interval: float,
466
+ ) -> str:
467
+ doc, status = await poll_trajectory(
468
+ sessions, agent_slug, session_id, wait, poll_interval=poll_interval
469
+ )
470
+ session_path = f"/api/agents/{encode(agent_slug)}/sessions/{encode(session_id)}/trajectory"
471
+ progress(await _session_summary(api, session_path, session_id))
472
+ steps = len((doc or {}).get("steps") or [])
473
+ progress(f" trajectory: {status}, {steps} steps")
474
+ return status
475
+
476
+
477
+ async def _dry_run(
478
+ api: AsyncApi,
479
+ local: LocalConfig,
480
+ spec: TaskSpec,
481
+ task: Task,
482
+ *,
483
+ dataset_ref: Reference,
484
+ agent_ref: Reference,
485
+ me: str,
486
+ selection: Selection,
487
+ count: int,
488
+ model: str | None,
489
+ wait: int,
490
+ provider: Any,
491
+ progress: Progress,
492
+ poll_interval: float,
493
+ ) -> ExperimentResult:
494
+ store = HttpStore(api)
495
+ cases = (await store.cases_for(slug(dataset_ref), selection, agent=slug(agent_ref)))[:count]
496
+ blob = HttpBlobStore(api, None)
497
+ deps = RunnerDeps(
498
+ cfg=local,
499
+ store=store,
500
+ blob_store=blob,
501
+ agents=HttpAgents(api),
502
+ evaluators=HttpEvaluators(api),
503
+ credentials=_credentials(local, me),
504
+ progress=progress,
505
+ sessions=HttpSessionReader(api),
506
+ )
507
+ summaries: list[RunSummary] = []
508
+ for case in cases:
509
+ outcome = await call_task_traced(
510
+ task,
511
+ spec,
512
+ blob=blob,
513
+ case=case,
514
+ repetition=1,
515
+ exp_id=None,
516
+ model=model,
517
+ provider=provider,
518
+ )
519
+ progress(f"dry run {case.case_id}: {outcome.status}")
520
+ progress(f" answer: {outcome.answer if outcome.answer is not None else outcome.output}")
521
+ if outcome.error:
522
+ progress(f" error: {outcome.error}")
523
+ status: str | None = None
524
+ if outcome.trajectory is not None:
525
+ status = "valid"
526
+ progress(f" pushed trajectory: {len(outcome.trajectory.get('steps') or [])} steps")
527
+ elif outcome.session_id is not None:
528
+ assert deps.sessions is not None
529
+ try:
530
+ status = await _report_session(
531
+ api,
532
+ deps.sessions,
533
+ slug(agent_ref),
534
+ outcome.session_id,
535
+ wait=wait,
536
+ progress=progress,
537
+ poll_interval=poll_interval,
538
+ )
539
+ except ApiRequestError as exc:
540
+ status = "unavailable"
541
+ progress(f" session {outcome.session_id}: trajectory read failed: {exc}")
542
+ else:
543
+ status = "unavailable"
544
+ summaries.append(
545
+ RunSummary(
546
+ case_id=case.case_id,
547
+ repetition=1,
548
+ status=outcome.status,
549
+ trajectory_status=status,
550
+ session_id=outcome.session_id,
551
+ )
552
+ )
553
+ return ExperimentResult(experiment_id=None, url=None, runs=summaries)
554
+
555
+
556
+ async def arun_experiment(
557
+ *,
558
+ dataset: str,
559
+ agent: str,
560
+ task: Task,
561
+ workspace: str | None = None,
562
+ evaluators: Sequence[PythonEvaluator] = (),
563
+ repetitions: int = 1,
564
+ concurrency: int | None = None,
565
+ case_ids: Sequence[str] | None = None,
566
+ split: str | None = None,
567
+ model: str | None = None,
568
+ resume: int | None = None,
569
+ dry_run: bool | int = False,
570
+ trajectory_wait_seconds: int | None = None,
571
+ judge: JudgeSettings | None = None,
572
+ api_transport: httpx.AsyncBaseTransport | None = None,
573
+ custom_eval_transport: httpx.AsyncBaseTransport | None = None,
574
+ progress: Progress = print,
575
+ poll_interval: float = 5.0,
576
+ tracer_provider: Any = None,
577
+ ) -> ExperimentResult:
578
+ """The async form of `run_experiment`, for code already in an event loop (a notebook).
579
+
580
+ Same arguments and result as `run_experiment`. The arguments after `judge` exist
581
+ for tests.
582
+ """
583
+ local = _local_config().resolve_env(scoring=not dry_run)
584
+ spec = inspect_task(task)
585
+ if model is not None and ignores_model(spec):
586
+ progress(
587
+ f"warning: model={model} is recorded on the experiment, but the task has no `model` "
588
+ "parameter, so the agent never sees it"
589
+ )
590
+ active = connection.active()
591
+ api = AsyncApi(active.url, active.token, transport=api_transport)
592
+ try:
593
+ await api.require_same_version()
594
+ me = await _username(api, active, progress)
595
+ definition = await _agent_definition(api, agent, me)
596
+ await _check_evaluator_names(evaluators, api, definition.workspace)
597
+ dataset_ref = resolve_dataset(dataset, workspace, me)
598
+ agent_ref = definition.reference
599
+ wait = (
600
+ trajectory_wait_seconds
601
+ if trajectory_wait_seconds is not None
602
+ else definition.settings.trajectory_wait_seconds
603
+ )
604
+ provider = tracer_provider if tracer_provider is not None else trace.get_tracer_provider()
605
+ on_disk = local.cases / dataset_ref.name if local.cases is not None else None
606
+ own_dataset = dataset_ref.workspace == me and resume is None
607
+ if own_dataset and on_disk is None:
608
+ progress(
609
+ f"no cases: directory in {local.path}; running {slug(dataset_ref)} as the "
610
+ "server holds it, without syncing"
611
+ )
612
+ if own_dataset and on_disk is not None and on_disk.is_dir():
613
+ await asyncio.to_thread(
614
+ sync_dataset,
615
+ SyncApi(active.url, active.token),
616
+ on_disk,
617
+ name=dataset_ref.name,
618
+ workspace=me,
619
+ revision=git_revision(on_disk),
620
+ fixtures_dir=local.fixtures,
621
+ )
622
+ python_evaluators = {e.name: e for e in evaluators}
623
+ if dry_run:
624
+ return await _dry_run(
625
+ api,
626
+ local,
627
+ spec,
628
+ task,
629
+ dataset_ref=dataset_ref,
630
+ agent_ref=agent_ref,
631
+ me=me,
632
+ selection=_selection(case_ids, split),
633
+ count=int(dry_run),
634
+ model=model,
635
+ wait=wait,
636
+ provider=provider,
637
+ progress=progress,
638
+ poll_interval=poll_interval,
639
+ )
640
+ store = HttpStore(api)
641
+ holder = local_holder(me)
642
+ credentials = _credentials(local, me)
643
+ if resume is None:
644
+ resolved_judge = judge or JudgeSettings.model_validate(
645
+ (await api.get("/api/judge-default")).json()
646
+ )
647
+ selected = await store.cases_for(
648
+ slug(dataset_ref),
649
+ _selection(case_ids, split),
650
+ agent=slug(agent_ref),
651
+ )
652
+ judge_key_for(credentials, resolved_judge, selected)
653
+ revision = git_revision(on_disk) if on_disk is not None and on_disk.is_dir() else None
654
+ exp, dropped = await store.start_experiment(
655
+ ExperimentRequest(
656
+ dataset=slug(dataset_ref),
657
+ agent=slug(agent_ref),
658
+ selection=_selection(case_ids, split),
659
+ repetitions=repetitions,
660
+ model=model,
661
+ capabilities=sdk_capabilities(definition),
662
+ cases=revision,
663
+ evalkit_version=version.current(),
664
+ code_evaluators=sorted(python_evaluators) or None,
665
+ judge=resolved_judge,
666
+ holder=holder,
667
+ trajectory_wait_seconds=trajectory_wait_seconds,
668
+ )
669
+ )
670
+ if dropped:
671
+ progress(dropped_cases_text(dropped))
672
+ else:
673
+ exp = await store.transition(resume, ExperimentStatus.RUNNING, holder)
674
+ progress(f"experiment {exp.id}")
675
+ blob = HttpBlobStore(api, exp.id)
676
+ deps = RunnerDeps(
677
+ cfg=local,
678
+ store=store,
679
+ blob_store=blob,
680
+ agents=HttpAgents(api),
681
+ evaluators=HttpEvaluators(api),
682
+ credentials=credentials,
683
+ custom_eval_transport=custom_eval_transport,
684
+ progress=progress,
685
+ sessions=HttpSessionReader(api),
686
+ python_evaluators=python_evaluators,
687
+ )
688
+ cases = {case.case_id: case for case in await store.pinned_cases(exp.id)}
689
+ for line in await missing_local_keys(deps, definition, cases.values()):
690
+ progress(line)
691
+ judge_key(deps, exp.config.judge, cases.values())
692
+ slots = asyncio.Semaphore(max(concurrency or definition.settings.max_concurrent_runs, 1))
693
+ summaries: list[RunSummary] = []
694
+ used_sessions: set[str] = set()
695
+
696
+ async def one(case: CaseView, repetition: int) -> None:
697
+ async with slots:
698
+ outcome = await call_task_traced(
699
+ task,
700
+ spec,
701
+ blob=blob,
702
+ case=case,
703
+ repetition=repetition,
704
+ exp_id=exp.id,
705
+ model=model,
706
+ provider=provider,
707
+ )
708
+ summaries.append(
709
+ await settle_and_record(
710
+ deps,
711
+ exp,
712
+ case,
713
+ repetition,
714
+ outcome,
715
+ wait_seconds=wait,
716
+ model=model,
717
+ poll_interval=poll_interval,
718
+ holder=holder,
719
+ used_sessions=used_sessions,
720
+ )
721
+ )
722
+
723
+ async def work() -> None:
724
+ await asyncio.gather(
725
+ *(
726
+ one(cases[pending.case_id], pending.repetition)
727
+ for pending in await store.pending_runs(exp.id)
728
+ )
729
+ )
730
+ await store.transition(exp.id, ExperimentStatus.EVALUATING, holder)
731
+ await evaluate_experiment(deps, exp.id, holder)
732
+
733
+ try:
734
+ await run_leased(deps, exp.id, holder, work())
735
+ except LeaseLost as exc:
736
+ note = f"resume it with run_experiment(..., resume={exp.id})"
737
+ exc.add_note(note)
738
+ progress(f"{exc}; {note}")
739
+ raise
740
+ except (ScoringFailed, ApiRequestError, httpx.TransportError) as exc:
741
+ _note_how_to_finish(exc, exp.id, sorted(python_evaluators), progress)
742
+ raise
743
+ report = (await api.get(f"/api/experiments/{exp.id}/scorecard")).json()
744
+ for line in summary_lines(report):
745
+ progress(line)
746
+ return ExperimentResult(
747
+ experiment_id=exp.id,
748
+ url=_experiment_url(active.url, exp.id),
749
+ runs=sorted(summaries, key=lambda s: (s.case_id, s.repetition)),
750
+ )
751
+ finally:
752
+ await api.aclose()
753
+
754
+
755
+ async def aevaluate_experiment(
756
+ *,
757
+ experiment_id: int,
758
+ evaluators: Sequence[PythonEvaluator] = (),
759
+ refetch_trajectories: bool = False,
760
+ api_transport: httpx.AsyncBaseTransport | None = None,
761
+ custom_eval_transport: httpx.AsyncBaseTransport | None = None,
762
+ progress: Progress = print,
763
+ ) -> ExperimentResult:
764
+ """The async form of `evaluate_experiment`, for code already in an event loop.
765
+
766
+ Same arguments and result as `evaluate_experiment`. The arguments after
767
+ `refetch_trajectories` exist for tests.
768
+ """
769
+ local = _local_config().resolve_env(scoring=True)
770
+ active = connection.active()
771
+ api = AsyncApi(active.url, active.token, transport=api_transport)
772
+ try:
773
+ await api.require_same_version()
774
+ store = HttpStore(api)
775
+ exp = await store.get_experiment(experiment_id)
776
+ await _check_evaluator_names(evaluators, api, parse_slug(exp.agent).workspace)
777
+ declared = set(exp.config.snapshot.code_evaluators)
778
+ unknown = sorted({e.name for e in evaluators} - declared)
779
+ if unknown:
780
+ raise ValueError(
781
+ f"python evaluators {', '.join(unknown)} were not part of experiment "
782
+ f"{experiment_id} (declared: {sorted(declared) or 'none'})"
783
+ )
784
+ me = active.username or ""
785
+ holder = local_holder(me)
786
+ deps = RunnerDeps(
787
+ cfg=local,
788
+ store=store,
789
+ blob_store=HttpBlobStore(api, experiment_id),
790
+ agents=HttpAgents(api),
791
+ evaluators=HttpEvaluators(api),
792
+ credentials=_credentials(local, me),
793
+ custom_eval_transport=custom_eval_transport,
794
+ progress=progress,
795
+ sessions=HttpSessionReader(api),
796
+ python_evaluators={e.name: e for e in evaluators},
797
+ )
798
+ await store.transition(experiment_id, ExperimentStatus.EVALUATING, holder)
799
+ try:
800
+ await rescore_experiment(
801
+ deps, experiment_id, holder, refetch_trajectories=refetch_trajectories
802
+ )
803
+ except (ScoringFailed, ApiRequestError, httpx.TransportError) as exc:
804
+ _note_how_to_finish(exc, experiment_id, sorted(declared), progress)
805
+ raise
806
+ runs = [
807
+ RunSummary(
808
+ case_id=r.case_id,
809
+ repetition=r.repetition,
810
+ status=r.agent_run.status.value,
811
+ trajectory_status=r.agent_run.trajectory_status.value
812
+ if r.agent_run.trajectory_status
813
+ else None,
814
+ session_id=r.agent_run.harness_run_id,
815
+ )
816
+ for r in await store.run_details(experiment_id)
817
+ ]
818
+ return ExperimentResult(
819
+ experiment_id=experiment_id,
820
+ url=_experiment_url(active.url, experiment_id),
821
+ runs=runs,
822
+ )
823
+ finally:
824
+ await api.aclose()
825
+
826
+
827
+ def _loop_is_running() -> bool:
828
+ try:
829
+ asyncio.get_running_loop()
830
+ except RuntimeError:
831
+ return False
832
+ return True
833
+
834
+
835
+ def run_experiment(
836
+ *,
837
+ dataset: str,
838
+ agent: str,
839
+ task: Task,
840
+ workspace: str | None = None,
841
+ evaluators: Sequence[PythonEvaluator] = (),
842
+ repetitions: int = 1,
843
+ concurrency: int | None = None,
844
+ case_ids: Sequence[str] | None = None,
845
+ split: str | None = None,
846
+ model: str | None = None,
847
+ resume: int | None = None,
848
+ dry_run: bool | int = False,
849
+ trajectory_wait_seconds: int | None = None,
850
+ judge: JudgeSettings | None = None,
851
+ ) -> ExperimentResult:
852
+ """Run an experiment: call `task` once per case and repetition, store the runs on
853
+ the server and score them.
854
+
855
+ Scoring uses the judge, the remote evaluators of the agent's workspace and the
856
+ Python `evaluators` you pass. Reads `evalkit.yaml` (cases, fixtures, credentials)
857
+ from the current directory or a parent, and the login from `evalkit login`.
858
+
859
+ Args:
860
+ dataset: The dataset to run. A bare name means your personal workspace; if it
861
+ is also a directory under `cases` in `evalkit.yaml`, it is synced first.
862
+ `name@workspace` runs a shared dataset, every review state included.
863
+ agent: The code-driven agent on the server that the runs belong to, created once
864
+ with `evalkit agents create`. A bare name means your personal workspace.
865
+ task: Your function, called once per case and repetition, sync or async. Its
866
+ parameters bind by name from `input` (the case input), `files`
867
+ (`list[Fixture]`), `model` and `metadata` (`session_id`, `experiment_id`,
868
+ `case_id`, `repetition`, `tags`). A task with one parameter of another name
869
+ receives the input. Return a `str` (the answer), a `dict` (the structured
870
+ output) or a `RunResult`. A task that raises records a failed run.
871
+ workspace: The workspace of `dataset` when it is a bare name.
872
+ evaluators: Python evaluators made with `evaluator`. A case selects one by name
873
+ under `evaluators.custom`, like an HTTP evaluator.
874
+ repetitions: How many times each case runs.
875
+ concurrency: How many tasks run at once. Defaults to the agent's
876
+ `max_concurrent_runs`.
877
+ case_ids: Run only these cases.
878
+ split: Run only the cases of this split (a case tag).
879
+ model: Passed to the task's `model` parameter and recorded on the experiment.
880
+ resume: An experiment id to resume: runs only the cases it has not finished.
881
+ dry_run: `True` runs one case without creating an experiment and prints the
882
+ answer and what the server made of the session; an int runs that many.
883
+ trajectory_wait_seconds: How long to wait for a run's spans to settle into a
884
+ trajectory. Defaults to the agent's setting.
885
+ judge: The judge for LLM-graded evaluators. Defaults to the server's default
886
+ judge. Its API key comes from `credentials.judges` in `evalkit.yaml`, keyed
887
+ by `base_url`.
888
+
889
+ Returns:
890
+ The experiment id, its URL and one summary per run.
891
+
892
+ Raises:
893
+ RuntimeError: When called from a running event loop; await `arun_experiment`
894
+ there instead.
895
+ """
896
+ if _loop_is_running():
897
+ raise RuntimeError(
898
+ "run_experiment cannot be called from a running event loop; "
899
+ "await arun_experiment(...) instead"
900
+ )
901
+ return asyncio.run(
902
+ arun_experiment(
903
+ dataset=dataset,
904
+ agent=agent,
905
+ task=task,
906
+ workspace=workspace,
907
+ evaluators=evaluators,
908
+ repetitions=repetitions,
909
+ concurrency=concurrency,
910
+ case_ids=case_ids,
911
+ split=split,
912
+ model=model,
913
+ resume=resume,
914
+ dry_run=dry_run,
915
+ trajectory_wait_seconds=trajectory_wait_seconds,
916
+ judge=judge,
917
+ )
918
+ )
919
+
920
+
921
+ def evaluate_experiment_sync(
922
+ *,
923
+ experiment_id: int,
924
+ evaluators: Sequence[PythonEvaluator] = (),
925
+ refetch_trajectories: bool = False,
926
+ ) -> ExperimentResult:
927
+ """Re-score an experiment that used Python evaluators.
928
+
929
+ `evalkit evaluate` refuses such experiments, because the evaluators live in your
930
+ script; call this from the script instead. Exported as `evaluate_experiment`.
931
+
932
+ Args:
933
+ experiment_id: The experiment to re-score.
934
+ evaluators: The Python evaluators the experiment ran with. Each must have been
935
+ part of the experiment.
936
+ refetch_trajectories: Read the trajectories of runs whose trajectory was
937
+ `partial` or `unavailable` again before scoring.
938
+
939
+ Returns:
940
+ The experiment id, its URL and one summary per run.
941
+
942
+ Raises:
943
+ RuntimeError: When called from a running event loop; await
944
+ `aevaluate_experiment` there instead.
945
+ """
946
+ if _loop_is_running():
947
+ raise RuntimeError(
948
+ "evaluate_experiment cannot be called from a running event loop; "
949
+ "await aevaluate_experiment(...) instead"
950
+ )
951
+ return asyncio.run(
952
+ aevaluate_experiment(
953
+ experiment_id=experiment_id,
954
+ evaluators=evaluators,
955
+ refetch_trajectories=refetch_trajectories,
956
+ )
957
+ )