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/api/runs.py ADDED
@@ -0,0 +1,555 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import zipfile
5
+ from collections.abc import AsyncIterator, Awaitable, Callable
6
+ from contextlib import AsyncExitStack
7
+ from datetime import UTC, datetime
8
+ from posixpath import basename
9
+ from typing import IO, Annotated, Literal, cast
10
+
11
+ import httpx
12
+ from fastapi import APIRouter, HTTPException, Query
13
+ from pydantic import BaseModel, computed_field
14
+ from pydantic_core import from_json
15
+ from sqlalchemy.ext.asyncio import AsyncSession
16
+ from starlette.responses import StreamingResponse
17
+
18
+ from evalkit.api.deps import (
19
+ AgentStoreDep,
20
+ RunnerDepsDep,
21
+ SessionDep,
22
+ SessionReaderDep,
23
+ SpanStoreDep,
24
+ )
25
+ from evalkit.api.download import content_disposition
26
+ from evalkit.api.experiments import slug_or_400
27
+ from evalkit.contracts.atif import Trajectory, validate_trajectory
28
+ from evalkit.contracts.harness import FileEntry, FileList, RunListItem
29
+ from evalkit.core.agents import AgentDefinition, explicit, server_may_drive
30
+ from evalkit.core.blob import artifact_key, workspace_key
31
+ from evalkit.core.costmap import load_cost_map
32
+ from evalkit.core.harness import HarnessClient, HarnessFile
33
+ from evalkit.core.paths import is_safe_path, is_safe_run_id
34
+ from evalkit.core.pull import SCAN_NOTE, list_harness_runs
35
+ from evalkit.core.runner import RunnerDeps, case_title
36
+ from evalkit.core.trajectory import backfill_step_costs
37
+ from evalkit.db.agents import AgentStore
38
+ from evalkit.db.runs import (
39
+ AgentRunItem,
40
+ RunSource,
41
+ agent_run,
42
+ list_agent_runs,
43
+ runs_by_harness_ids,
44
+ )
45
+ from evalkit.db.sessions import DbSessionReader
46
+ from evalkit.db.spans import SessionSummary, SpanStore
47
+
48
+ router = APIRouter(prefix="/api/agents", tags=["agents"])
49
+
50
+ HARNESS_UNREACHABLE = "harness unreachable"
51
+ MAX_PAGE = 200
52
+
53
+
54
+ class BrowserRun(RunListItem):
55
+ status: str | None = None
56
+ experiment_id: int | None = None
57
+ experiment_number: int | None = None
58
+ case_id: str | None = None
59
+ stored: bool = False
60
+
61
+ @computed_field # type: ignore[prop-decorator]
62
+ @property
63
+ def source(self) -> Literal["app", "eval"]:
64
+ return "eval" if self.experiment_id is not None else "app"
65
+
66
+
67
+ class RunPage(BaseModel):
68
+ runs: list[BrowserRun]
69
+ total: int
70
+ harness_note: str | None = None
71
+
72
+
73
+ async def harness_client(deps: RunnerDeps, agent: AgentDefinition) -> HarnessClient:
74
+ assert deps.agent_keys is not None
75
+ return HarnessClient(
76
+ agent, await deps.agent_keys.agent_key(agent), transport=deps.harness_transport
77
+ )
78
+
79
+
80
+ async def agent_or_404(agents: AgentStore, agent: str) -> AgentDefinition:
81
+ return await agents.get(explicit(slug_or_400(agent)))
82
+
83
+
84
+ def _sdk_case(item: SessionSummary) -> tuple[str | None, str | None]:
85
+ if item.sdk_root is None:
86
+ return None, None
87
+ case_id = item.sdk_root.get("evalkit.case_id")
88
+ raw_input = item.sdk_root.get("input.value")
89
+ title = case_title(from_json(raw_input, allow_partial=True)) if raw_input else None
90
+ return (str(case_id) if case_id is not None else None), title
91
+
92
+
93
+ def _stored(item: AgentRunItem) -> BrowserRun:
94
+ return BrowserRun(
95
+ run_id=item.harness_run_id,
96
+ title=item.title,
97
+ user_id=item.created_by,
98
+ created_at=item.started_at,
99
+ status=item.status.value,
100
+ experiment_id=item.experiment_id,
101
+ experiment_number=item.experiment_number,
102
+ case_id=item.case_id,
103
+ stored=True,
104
+ )
105
+
106
+
107
+ def _merge(stored: list[BrowserRun], harness: list[RunListItem], limit: int) -> list[BrowserRun]:
108
+ seen = {run.run_id for run in stored}
109
+ extra = [BrowserRun(**run.model_dump()) for run in harness if run.run_id not in seen]
110
+ oldest = datetime.min.replace(tzinfo=UTC)
111
+ merged = sorted([*stored, *extra], key=lambda run: run.created_at or oldest, reverse=True)
112
+ return merged[:limit]
113
+
114
+
115
+ async def resolve_runs(
116
+ record: AgentDefinition,
117
+ deps: RunnerDeps,
118
+ spans: SpanStore,
119
+ session: AsyncSession,
120
+ *,
121
+ source: RunSource = "all",
122
+ since: datetime | None = None,
123
+ until: datetime | None = None,
124
+ query: str | None = None,
125
+ user_id: str | None = None,
126
+ client_id: str | None = None,
127
+ limit: int = 50,
128
+ offset: int = 0,
129
+ no_session: bool = False,
130
+ run_ids: list[str] | None = None,
131
+ ) -> RunPage:
132
+ if run_ids is not None:
133
+ known = await runs_by_harness_ids(session, record.id, run_ids)
134
+ rows = [_stored(known[run_id]) for run_id in run_ids if run_id in known]
135
+ return RunPage(runs=rows, total=len(rows))
136
+ if record.is_otel:
137
+ if no_session:
138
+ items, total = await spans.list_unsessioned(record.id, limit=limit, offset=offset)
139
+ else:
140
+ items, total = await spans.list_sessions(
141
+ record.id,
142
+ query=query,
143
+ limit=limit,
144
+ offset=offset,
145
+ since=since,
146
+ until=until,
147
+ linked=source,
148
+ )
149
+ linked = await runs_by_harness_ids(
150
+ session, record.id, [i.session_id for i in items if i.session_id]
151
+ )
152
+ sessions: list[BrowserRun] = []
153
+ for i in items:
154
+ run = linked.get(i.session_id) if i.session_id else None
155
+ sdk_case_id, sdk_title = _sdk_case(i)
156
+ sessions.append(
157
+ BrowserRun(
158
+ run_id=i.session_id or i.trace_id,
159
+ title=(run.title if run else None) or sdk_title or i.title,
160
+ created_at=i.created_at,
161
+ user_id=run.created_by if run else None,
162
+ status=run.status.value if run else None,
163
+ experiment_id=run.experiment_id if run else None,
164
+ experiment_number=run.experiment_number if run else None,
165
+ case_id=(run.case_id if run else None) or sdk_case_id,
166
+ stored=run is not None,
167
+ )
168
+ )
169
+ return RunPage(runs=sessions, total=total)
170
+ stored_items, stored_total = await list_agent_runs(
171
+ session,
172
+ record.id,
173
+ query=query,
174
+ limit=limit + offset,
175
+ created_by=user_id,
176
+ since=since,
177
+ until=until,
178
+ source=source,
179
+ )
180
+ stored = [_stored(item) for item in stored_items]
181
+ if source == "eval" or not server_may_drive(record):
182
+ return RunPage(runs=stored[offset : offset + limit], total=stored_total)
183
+ client = await harness_client(deps, record)
184
+ note: str | None = None
185
+ harness: list[RunListItem] = []
186
+ harness_total = 0
187
+
188
+ async def evals(run_ids: list[str]) -> set[str]:
189
+ known = await runs_by_harness_ids(session, record.id, run_ids)
190
+ return {run_id for run_id, item in known.items() if item.experiment_id is not None}
191
+
192
+ # Spec "Runs browser": an unreachable harness still lists the stored runs, with a note.
193
+ try:
194
+ listed = await list_harness_runs(
195
+ client,
196
+ wanted=limit + offset,
197
+ since=since,
198
+ until=until,
199
+ query=query,
200
+ user_id=user_id,
201
+ client_id=client_id,
202
+ exclude=evals if source == "app" else None,
203
+ )
204
+ harness, harness_total = listed.runs, listed.total
205
+ if listed.capped:
206
+ note = SCAN_NOTE
207
+ except httpx.HTTPError:
208
+ note = HARNESS_UNREACHABLE
209
+ finally:
210
+ await client.aclose()
211
+ harness_ids = {run.run_id for run in harness}
212
+ if client_id is not None:
213
+ stored = [run for run in stored if run.run_id in harness_ids]
214
+ stored_total = len(stored)
215
+ overlap = sum(1 for run in stored if run.run_id in harness_ids)
216
+ merged = _merge(stored, harness, limit + offset)[offset:]
217
+ return RunPage(
218
+ runs=merged,
219
+ total=max(stored_total + harness_total - overlap, len(merged) + offset),
220
+ harness_note=note,
221
+ )
222
+
223
+
224
+ @router.get("/{agent}/runs")
225
+ async def list_runs(
226
+ agent: str,
227
+ deps: RunnerDepsDep,
228
+ agents: AgentStoreDep,
229
+ spans: SpanStoreDep,
230
+ session: SessionDep,
231
+ query: str | None = None,
232
+ user_id: str | None = None,
233
+ client_id: str | None = None,
234
+ limit: int = 50,
235
+ offset: int = 0,
236
+ no_session: bool = False,
237
+ source: RunSource = "all",
238
+ since: datetime | None = None,
239
+ until: datetime | None = None,
240
+ run_id: Annotated[list[str] | None, Query()] = None,
241
+ ) -> dict:
242
+ page = await resolve_runs(
243
+ await agent_or_404(agents, agent),
244
+ deps,
245
+ spans,
246
+ session,
247
+ source=source,
248
+ since=since,
249
+ until=until,
250
+ query=query,
251
+ user_id=user_id,
252
+ client_id=client_id,
253
+ limit=min(limit, MAX_PAGE),
254
+ offset=offset,
255
+ no_session=no_session,
256
+ run_ids=run_id,
257
+ )
258
+ return page.model_dump(
259
+ mode="json", exclude={"harness_note"} if page.harness_note is None else None
260
+ )
261
+
262
+
263
+ async def read_trajectory(
264
+ record: AgentDefinition,
265
+ run_id: str,
266
+ deps: RunnerDeps,
267
+ sessions: DbSessionReader,
268
+ trajectory_key: str | None,
269
+ client: HarnessClient | None,
270
+ ) -> Trajectory:
271
+ if trajectory_key:
272
+ return validate_trajectory(json.loads(await deps.blob_store.get_object(trajectory_key)))
273
+ if record.is_otel:
274
+ result = await sessions.read(record.ref, run_id)
275
+ if result is None or result.trajectory is None:
276
+ raise HTTPException(status_code=404, detail="no spans for this session")
277
+ return validate_trajectory(result.trajectory)
278
+ if client is not None:
279
+ try:
280
+ return await client.trajectory(run_id)
281
+ except httpx.HTTPStatusError as exc:
282
+ raise HTTPException(
283
+ status_code=exc.response.status_code, detail="harness error"
284
+ ) from exc
285
+ raise HTTPException(status_code=404, detail="run has no stored trajectory")
286
+
287
+
288
+ async def fetch_trajectory(
289
+ record: AgentDefinition,
290
+ run_id: str,
291
+ deps: RunnerDeps,
292
+ sessions: DbSessionReader,
293
+ session: AsyncSession,
294
+ ) -> Trajectory:
295
+ stored = await agent_run(session, record.id, run_id)
296
+ key = stored.trajectory_key if stored is not None else None
297
+ client = await harness_client(deps, record) if not key and server_may_drive(record) else None
298
+ try:
299
+ return await read_trajectory(record, run_id, deps, sessions, key, client)
300
+ finally:
301
+ if client is not None:
302
+ await client.aclose()
303
+
304
+
305
+ @router.get("/{agent}/runs/{run_id}/trajectory")
306
+ async def live_trajectory(
307
+ agent: str,
308
+ run_id: str,
309
+ deps: RunnerDepsDep,
310
+ agents: AgentStoreDep,
311
+ sessions: SessionReaderDep,
312
+ session: SessionDep,
313
+ ) -> dict:
314
+ _run_id_or_400(run_id)
315
+ record = await agent_or_404(agents, agent)
316
+ trajectory = await fetch_trajectory(record, run_id, deps, sessions, session)
317
+ # Price any step the harness left uncosted so the cost view matches the
318
+ # scorecard gauge (see backfill_step_costs).
319
+ backfill_step_costs(trajectory, load_cost_map(deps.cfg.cost_map))
320
+ return trajectory.model_dump(exclude_none=True)
321
+
322
+
323
+ NOT_ON_HARNESS = "not found on the harness: wrong path, or the harness no longer has it"
324
+
325
+
326
+ def _run_id_or_400(run_id: str) -> str:
327
+ if not is_safe_run_id(run_id):
328
+ raise HTTPException(status_code=400, detail="run id must be one plain path segment")
329
+ return run_id
330
+
331
+
332
+ def _path_or_400(path: str) -> str:
333
+ if not is_safe_path(path):
334
+ raise HTTPException(status_code=400, detail="path must have no empty, '.' or '..' segments")
335
+ return path.lstrip("/")
336
+
337
+
338
+ def _harness_status(exc: httpx.HTTPStatusError) -> HTTPException:
339
+ if exc.response.status_code == 404:
340
+ return HTTPException(status_code=404, detail=NOT_ON_HARNESS)
341
+ return HTTPException(status_code=exc.response.status_code, detail="harness error")
342
+
343
+
344
+ async def _stored_files(deps: RunnerDeps, record: AgentDefinition, run_id: str) -> dict[str, int]:
345
+ prefix = artifact_key(record.id, run_id, "")
346
+ return {
347
+ key.removeprefix(prefix): size for key, size in await deps.blob_store.list_objects(prefix)
348
+ }
349
+
350
+
351
+ async def _snapshot(deps: RunnerDeps, record: AgentDefinition, run_id: str) -> dict[str, FileEntry]:
352
+ key = workspace_key(record.id, run_id)
353
+ if not await deps.blob_store.object_exists(key):
354
+ return {}
355
+ tree = FileList.model_validate_json(await deps.blob_store.get_object(key))
356
+ return {entry.path: entry for entry in tree.files}
357
+
358
+
359
+ async def _listing(
360
+ deps: RunnerDeps, record: AgentDefinition, run_id: str
361
+ ) -> tuple[dict[str, FileEntry], set[str]]:
362
+ stored = await _stored_files(deps, record, run_id)
363
+ snapshot = await _snapshot(deps, record, run_id)
364
+ if snapshot or stored or not server_may_drive(record):
365
+ kept = {path: FileEntry(path=path, size=size) for path, size in stored.items()}
366
+ return {**kept, **snapshot}, set(stored)
367
+ client = await harness_client(deps, record)
368
+ try:
369
+ files = await client.list_files(run_id)
370
+ except httpx.HTTPStatusError as exc:
371
+ raise _harness_status(exc) from exc
372
+ finally:
373
+ await client.aclose()
374
+ return {entry.path: entry for entry in files.files}, set()
375
+
376
+
377
+ @router.get("/{agent}/runs/{run_id}/files")
378
+ async def list_files(agent: str, run_id: str, deps: RunnerDepsDep, agents: AgentStoreDep) -> dict:
379
+ _run_id_or_400(run_id)
380
+ record = await agent_or_404(agents, agent)
381
+ listing, stored = await _listing(deps, record, run_id)
382
+ return {
383
+ "files": [
384
+ {**entry.model_dump(), "stored": path in stored}
385
+ for path, entry in sorted(listing.items())
386
+ ]
387
+ }
388
+
389
+
390
+ LOGGER = logging.getLogger(__name__)
391
+
392
+ # A single oversized file is refused before the response starts; once headers
393
+ # are out an error can only truncate the zip.
394
+ MAX_FILE_BYTES = 512 * 1024 * 1024
395
+
396
+
397
+ class _ChunkSink:
398
+ def __init__(self) -> None:
399
+ self.buffer = bytearray()
400
+
401
+ def write(self, data: bytes) -> int:
402
+ self.buffer.extend(data)
403
+ return len(data)
404
+
405
+ def flush(self) -> None:
406
+ pass
407
+
408
+ def drain(self) -> bytes:
409
+ chunk = bytes(self.buffer)
410
+ self.buffer.clear()
411
+ return chunk
412
+
413
+
414
+ # zipfile writes to the unseekable sink with data descriptors, so every entry
415
+ # is flushed to the client as soon as it is compressed and the archive never
416
+ # sits in memory as a whole. A fetch failure after the headers are out cannot
417
+ # become a status code; re-raising drops the connection so the browser shows
418
+ # a failed download rather than a quietly smaller zip.
419
+ async def _zip_stream(
420
+ entries: list[tuple[str, str]],
421
+ fetch: Callable[[str], Awaitable[AsyncIterator[bytes]]],
422
+ close: Callable[[], Awaitable[None]],
423
+ ) -> AsyncIterator[bytes]:
424
+ sink = _ChunkSink()
425
+ try:
426
+ with zipfile.ZipFile(cast(IO[bytes], sink), "w", zipfile.ZIP_DEFLATED) as archive:
427
+ for arcname, path in entries:
428
+ try:
429
+ chunks = await fetch(path)
430
+ except httpx.HTTPStatusError as exc:
431
+ LOGGER.warning(
432
+ "zip aborted: harness returned %s for %s", exc.response.status_code, path
433
+ )
434
+ raise
435
+ with archive.open(arcname, "w", force_zip64=True) as entry:
436
+ async for chunk in chunks:
437
+ await asyncio.to_thread(entry.write, chunk)
438
+ yield sink.drain()
439
+ yield sink.drain()
440
+ yield sink.drain()
441
+ finally:
442
+ await close()
443
+
444
+
445
+ def _zip_entries(
446
+ listing: dict[str, int], folder: str, fallback_name: str
447
+ ) -> tuple[str, list[tuple[str, str]]]:
448
+ prefix = f"{folder}/" if folder else ""
449
+ root = basename(folder) if folder else fallback_name
450
+ selected = sorted(p for p in listing if p.lstrip("/").startswith(prefix) and is_safe_path(p))
451
+ if not selected:
452
+ raise HTTPException(status_code=404, detail=f"no files under {folder or '/'}")
453
+ largest = max(selected, key=lambda p: listing[p])
454
+ if listing[largest] > MAX_FILE_BYTES:
455
+ raise HTTPException(
456
+ status_code=413,
457
+ detail=f"{largest} is {listing[largest]} bytes; zip entries stop at {MAX_FILE_BYTES}",
458
+ )
459
+ return root, [(f"{root}/{p.lstrip('/')[len(prefix) :]}", p) for p in selected]
460
+
461
+
462
+ def _zip_response(
463
+ root: str,
464
+ entries: list[tuple[str, str]],
465
+ fetch: Callable[[str], Awaitable[AsyncIterator[bytes]]],
466
+ close: Callable[[], Awaitable[None]],
467
+ ) -> StreamingResponse:
468
+ return StreamingResponse(
469
+ _zip_stream(entries, fetch, close),
470
+ media_type="application/zip",
471
+ headers=content_disposition(f"{root}.zip"),
472
+ )
473
+
474
+
475
+ async def _noop() -> None:
476
+ return None
477
+
478
+
479
+ @router.get("/{agent}/runs/{run_id}/zip")
480
+ async def zip_files(
481
+ agent: str, run_id: str, deps: RunnerDepsDep, agents: AgentStoreDep, path: str = ""
482
+ ) -> StreamingResponse:
483
+ _run_id_or_400(run_id)
484
+ folder = path.strip("/")
485
+ if folder:
486
+ _path_or_400(folder)
487
+ agent_def = await agent_or_404(agents, agent)
488
+ listing, stored = await _listing(deps, agent_def, run_id)
489
+ drivable = server_may_drive(agent_def)
490
+ if not drivable:
491
+ listing = {p: entry for p, entry in listing.items() if p in stored}
492
+ sizes = {p: entry.size for p, entry in listing.items()}
493
+ root, entries = _zip_entries(sizes, folder, run_id)
494
+ prefix = artifact_key(agent_def.id, run_id, "")
495
+
496
+ async def stored_chunks(p: str) -> AsyncIterator[bytes]:
497
+ return (await deps.blob_store.get_stream(f"{prefix}{p}")).chunks
498
+
499
+ if all(p in stored for _, p in entries):
500
+ return _zip_response(root, entries, stored_chunks, _noop)
501
+ client = await harness_client(deps, agent_def)
502
+
503
+ async def fetch(p: str) -> AsyncIterator[bytes]:
504
+ if p in stored:
505
+ return await stored_chunks(p)
506
+ return (await client.stream_file(run_id, p)).chunks
507
+
508
+ return _zip_response(root, entries, fetch, client.aclose)
509
+
510
+
511
+ async def _closing(file: HarnessFile, client: HarnessClient) -> AsyncIterator[bytes]:
512
+ try:
513
+ async for chunk in file.chunks:
514
+ yield chunk
515
+ finally:
516
+ await client.aclose()
517
+
518
+
519
+ @router.get("/{agent}/runs/{run_id}/files/{path:path}")
520
+ async def get_file(
521
+ agent: str,
522
+ run_id: str,
523
+ path: str,
524
+ deps: RunnerDepsDep,
525
+ agents: AgentStoreDep,
526
+ download: bool = False,
527
+ ) -> StreamingResponse:
528
+ _run_id_or_400(run_id)
529
+ path = _path_or_400(path)
530
+ headers = content_disposition(basename(path)) if download else None
531
+ record = await agent_or_404(agents, agent)
532
+ key = artifact_key(record.id, run_id, path)
533
+ if await deps.blob_store.object_exists(key):
534
+ blob = await deps.blob_store.get_stream(key)
535
+ return StreamingResponse(
536
+ blob.chunks,
537
+ media_type="application/octet-stream",
538
+ headers={"Content-Length": str(blob.size), **(headers or {})},
539
+ )
540
+ if server_may_drive(record):
541
+ client = await harness_client(deps, record)
542
+ async with AsyncExitStack() as cleanup:
543
+ cleanup.push_async_callback(client.aclose)
544
+ try:
545
+ file = await client.stream_file(run_id, path)
546
+ except httpx.HTTPStatusError as exc:
547
+ raise _harness_status(exc) from exc
548
+ cleanup.pop_all()
549
+ length = {"Content-Length": str(file.size)} if file.size is not None else {}
550
+ return StreamingResponse(
551
+ _closing(file, client),
552
+ media_type="application/octet-stream",
553
+ headers={**length, **(headers or {})},
554
+ )
555
+ raise HTTPException(status_code=404, detail="no such file")
@@ -0,0 +1,44 @@
1
+ import logging
2
+ from dataclasses import replace
3
+
4
+ from fastapi import Request
5
+
6
+ from evalkit.config import Config
7
+ from evalkit.core.agents import AgentDefinition
8
+ from evalkit.core.endpoints import check_endpoint
9
+ from evalkit.core.runner import RunnerDeps
10
+ from evalkit.db.keys import KeyLoader, StoredKeys
11
+
12
+ LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ async def server_keys(
16
+ request: Request, agent: AgentDefinition, judge_base_url: str | None
17
+ ) -> tuple[StoredKeys, list[str]]:
18
+ cfg: Config = request.app.state.cfg
19
+ deps: RunnerDeps = request.app.state.runner_deps
20
+ loader: KeyLoader = request.app.state.keys
21
+ for url in (agent.harness_url, judge_base_url):
22
+ if url is not None:
23
+ await check_endpoint(url, allow_private=cfg.allow_private_endpoints)
24
+ keys = await loader.load(agent, judge_base_url)
25
+ warnings = []
26
+ if agent.harness_url is not None and keys.key_for("agent", agent.ref) is None:
27
+ warnings.append(
28
+ f"agent {agent.ref} has no API key; calling its harness without x-api-key. "
29
+ f"Set one with evalkit agents edit {agent.ref} --api-key-env NAME"
30
+ )
31
+ for evaluator in (await deps.evaluators.in_workspace(agent.workspace)).values():
32
+ if keys.key_for("evaluator", evaluator.ref) is None:
33
+ warnings.append(
34
+ f"evaluator {evaluator.ref} has no API key; calling it without x-api-key"
35
+ )
36
+ if judge_base_url is not None and keys.require_judge_key(judge_base_url) is None:
37
+ warnings.append(f"LLM provider {judge_base_url} has no API key; judging without one")
38
+ for warning in warnings:
39
+ LOGGER.warning("%s", warning)
40
+ return keys, warnings
41
+
42
+
43
+ def with_keys(deps: RunnerDeps, keys: StoredKeys) -> RunnerDeps:
44
+ return replace(deps, credentials=keys)
evalkit/api/tokens.py ADDED
@@ -0,0 +1,51 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+
4
+ from evalkit.api.access import ActorDep, ensure_active, require, workspace_ref
5
+ from evalkit.api.auth import CurrentUser
6
+ from evalkit.api.deps import AgentStoreDep, AgentTokensDep, SessionDep
7
+ from evalkit.core.access import can_write
8
+ from evalkit.core.agents import explicit
9
+ from evalkit.core.workspace import parse_reference
10
+
11
+ router = APIRouter(prefix="/api/tokens", tags=["tokens"])
12
+
13
+
14
+ class TokenRequest(BaseModel):
15
+ agent: str
16
+
17
+
18
+ @router.post("")
19
+ async def create_token(
20
+ body: TokenRequest,
21
+ agents: AgentStoreDep,
22
+ tokens: AgentTokensDep,
23
+ session: SessionDep,
24
+ actor: ActorDep,
25
+ user: CurrentUser,
26
+ ) -> dict:
27
+ try:
28
+ ref = parse_reference(body.agent, user.username)
29
+ except ValueError as exc:
30
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
31
+ record = await agents.get(explicit(ref))
32
+ ws = await workspace_ref(session, record.workspace)
33
+ require(can_write(actor, ws), f"you cannot mint tokens for {record.ref}")
34
+ ensure_active(ws)
35
+ if record.archived:
36
+ raise HTTPException(status_code=409, detail=f"agent {record.ref} is archived")
37
+ token, info = await tokens.create(agent_id=record.id, user_id=user.id)
38
+ return {"id": info.id, "token": token, "agent": info.agent}
39
+
40
+
41
+ @router.get("")
42
+ async def list_tokens(tokens: AgentTokensDep, user: CurrentUser) -> list[dict]:
43
+ rows = await tokens.list(user_id=None if user.is_admin else user.id)
44
+ return [r.model_dump(mode="json") for r in rows]
45
+
46
+
47
+ @router.delete("/{token_id}")
48
+ async def revoke_token(token_id: int, tokens: AgentTokensDep, user: CurrentUser) -> dict:
49
+ if not await tokens.revoke(token_id, user_id=None if user.is_admin else user.id):
50
+ raise HTTPException(status_code=404, detail="unknown token")
51
+ return {"ok": True}