evalkit-py 0.2.0.dev12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. evalkit/__init__.py +0 -0
  2. evalkit/api/__init__.py +0 -0
  3. evalkit/api/access.py +74 -0
  4. evalkit/api/agents.py +159 -0
  5. evalkit/api/app.py +360 -0
  6. evalkit/api/auth.py +225 -0
  7. evalkit/api/cli_dist.py +10 -0
  8. evalkit/api/datasets.py +169 -0
  9. evalkit/api/deps.py +79 -0
  10. evalkit/api/download.py +15 -0
  11. evalkit/api/evaluators.py +121 -0
  12. evalkit/api/experiments.py +616 -0
  13. evalkit/api/fixtures.py +95 -0
  14. evalkit/api/harness_check.py +48 -0
  15. evalkit/api/imports.py +180 -0
  16. evalkit/api/limits.py +84 -0
  17. evalkit/api/llm_providers.py +120 -0
  18. evalkit/api/mcp/__init__.py +0 -0
  19. evalkit/api/mcp/auth.py +100 -0
  20. evalkit/api/mcp/descriptions.py +336 -0
  21. evalkit/api/mcp/models.py +259 -0
  22. evalkit/api/mcp/server.py +101 -0
  23. evalkit/api/mcp/tools.py +844 -0
  24. evalkit/api/meta.py +19 -0
  25. evalkit/api/otel.py +350 -0
  26. evalkit/api/pull.py +197 -0
  27. evalkit/api/review.py +271 -0
  28. evalkit/api/runs.py +555 -0
  29. evalkit/api/server_keys.py +44 -0
  30. evalkit/api/tokens.py +51 -0
  31. evalkit/api/trace_imports.py +173 -0
  32. evalkit/api/trace_sources.py +49 -0
  33. evalkit/api/traces.py +196 -0
  34. evalkit/api/users.py +72 -0
  35. evalkit/api/workspaces.py +131 -0
  36. evalkit/bundled/contracts/atif-accepted.json +5 -0
  37. evalkit/bundled/contracts/custom-evaluator-api-v1.json +243 -0
  38. evalkit/bundled/contracts/harness-api-v1.json +938 -0
  39. evalkit/bundled/contracts/span-mapping-v1.json +314 -0
  40. evalkit/bundled/docs/concepts.md +26 -0
  41. evalkit/bundled/docs/config.md +38 -0
  42. evalkit/bundled/docs/harness-api.md +44 -0
  43. evalkit/bundled/docs/install.md +60 -0
  44. evalkit/bundled/docs/login.md +36 -0
  45. evalkit/bundled/docs/mappings.md +136 -0
  46. evalkit/bundled/docs/mcp.md +44 -0
  47. evalkit/bundled/docs/quickstart.md +142 -0
  48. evalkit/bundled/docs/sdk-runs.md +171 -0
  49. evalkit/bundled/docs/tracing.md +171 -0
  50. evalkit/bundled/docs/try-locally.md +288 -0
  51. evalkit/bundled/docs/workspaces.md +45 -0
  52. evalkit/bundled/skills/authoring-eval-cases/SKILL.md +387 -0
  53. evalkit/bundled/skills/authoring-eval-cases/references/overlays.md +83 -0
  54. evalkit/bundled/skills/authoring-eval-cases/references/tool-checks.md +71 -0
  55. evalkit/bundled/skills/connect-traces/SKILL.md +296 -0
  56. evalkit/bundled/skills/connect-traces/references/gen_ai.json +42 -0
  57. evalkit/bundled/skills/connect-traces/references/openinference.json +35 -0
  58. evalkit/bundled/skills/connect-traces/references/span-mapping-schema.md +88 -0
  59. evalkit/bundled/skills/eval-driven-development/SKILL.md +458 -0
  60. evalkit/bundled/skills/eval-driven-development/references/analyze-run.md +97 -0
  61. evalkit/bundled/skills/eval-driven-development/references/diagnose.md +130 -0
  62. evalkit/bundled/skills/eval-driven-development/references/synthesize-cases.md +87 -0
  63. evalkit/bundled/skills/eval-driven-development/references/synthesize.md +165 -0
  64. evalkit/bundled/skills/evalkit-deployment/SKILL.md +332 -0
  65. evalkit/bundled/skills/evalkit-deployment/config.example.yaml +49 -0
  66. evalkit/bundled/skills/evalkit-deployment/docker-compose.yml +51 -0
  67. evalkit/bundled/skills/evalkit-deployment/references/config-and-deploy.md +333 -0
  68. evalkit/bundled/skills/evalkit-deployment/references/harness-api.md +139 -0
  69. evalkit/bundled/skills/evalkit-deployment/try/.env.example +11 -0
  70. evalkit/bundled/skills/evalkit-deployment/try/agent.py +131 -0
  71. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/balance-sheet/cases.yaml +25 -0
  72. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/income-statement/cases.yaml +43 -0
  73. evalkit/bundled/skills/evalkit-deployment/try/config.yaml +17 -0
  74. evalkit/bundled/skills/evalkit-deployment/try/docker-compose.yml +82 -0
  75. evalkit/bundled/skills/evalkit-deployment/try/evalkit.yaml +4 -0
  76. evalkit/bundled/skills/evalkit-deployment/try/mapping.json +42 -0
  77. evalkit/bundled/skills/evalkit-deployment/try/run.py +33 -0
  78. evalkit/bundled/skills/evalkit-usage/SKILL.md +509 -0
  79. evalkit/bundled/skills/evalkit-usage/references/suite-fixtures.md +42 -0
  80. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  81. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  82. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  83. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  84. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  85. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  86. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  87. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  88. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  89. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  90. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  91. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  92. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  93. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  94. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  95. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  96. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  97. evalkit/bundled/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  98. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  99. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  100. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  101. evalkit/bundled/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  102. evalkit/bundled/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  103. evalkit/bundled/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  104. evalkit/bundled/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  105. evalkit/bundled/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  106. evalkit/bundled/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  107. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  108. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  109. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  110. evalkit/bundled/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  111. evalkit/bundled/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  112. evalkit/bundled/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  113. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  114. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  115. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  116. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  117. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  118. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  119. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  120. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  121. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  122. evalkit/bundled/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  123. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  124. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  125. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  126. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  127. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  128. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  129. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  130. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  131. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  132. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  133. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  134. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  135. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  136. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  137. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  138. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  139. evalkit/bundled/ui/assets/abap-08VXUWAP.js +1 -0
  140. evalkit/bundled/ui/assets/apex-BWPQTe0t.js +1 -0
  141. evalkit/bundled/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  142. evalkit/bundled/ui/assets/bat-i0X4ZdIN.js +1 -0
  143. evalkit/bundled/ui/assets/bicep-B5-_aFwp.js +2 -0
  144. evalkit/bundled/ui/assets/cameligo-DMUM7wLl.js +1 -0
  145. evalkit/bundled/ui/assets/clojure-Cm7r79vr.js +1 -0
  146. evalkit/bundled/ui/assets/codicon-DF1abBS2.ttf +0 -0
  147. evalkit/bundled/ui/assets/coffee-Ba7i2nA0.js +1 -0
  148. evalkit/bundled/ui/assets/cpp-C7h46wYY.js +1 -0
  149. evalkit/bundled/ui/assets/csharp-BKxtCVv1.js +1 -0
  150. evalkit/bundled/ui/assets/csp-bTuwJoIa.js +1 -0
  151. evalkit/bundled/ui/assets/css-DIMkf-bt.js +3 -0
  152. evalkit/bundled/ui/assets/css.worker-DwfUZ3fB.js +96 -0
  153. evalkit/bundled/ui/assets/cssMode-C97l1pbB.js +1 -0
  154. evalkit/bundled/ui/assets/cypher-CVaqCwHa.js +1 -0
  155. evalkit/bundled/ui/assets/dart-onAF5SnQ.js +1 -0
  156. evalkit/bundled/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  157. evalkit/bundled/ui/assets/ecl-94qehhXe.js +1 -0
  158. evalkit/bundled/ui/assets/editor-DjpsE8to.css +1 -0
  159. evalkit/bundled/ui/assets/editor.api-NTesIxw5.js +908 -0
  160. evalkit/bundled/ui/assets/editor.worker-CRz__AKW.js +33 -0
  161. evalkit/bundled/ui/assets/elixir-6RTg0lbw.js +1 -0
  162. evalkit/bundled/ui/assets/flow9-C5_-GSwl.js +1 -0
  163. evalkit/bundled/ui/assets/freemarker2-BWqEFagp.js +3 -0
  164. evalkit/bundled/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  165. evalkit/bundled/ui/assets/go-C-y9NEjX.js +1 -0
  166. evalkit/bundled/ui/assets/graphql-fmXr3nnJ.js +1 -0
  167. evalkit/bundled/ui/assets/handlebars-DkIF-whN.js +1 -0
  168. evalkit/bundled/ui/assets/hcl-CpzslTdj.js +1 -0
  169. evalkit/bundled/ui/assets/html-C_rpeQXJ.js +1 -0
  170. evalkit/bundled/ui/assets/html.worker-D9ugpX4y.js +509 -0
  171. evalkit/bundled/ui/assets/htmlMode-B0YIq3DF.js +1 -0
  172. evalkit/bundled/ui/assets/index-Cw-jBH6M.js +736 -0
  173. evalkit/bundled/ui/assets/index-DKtoQZtm.css +1 -0
  174. evalkit/bundled/ui/assets/ini-sBoK_t0W.js +1 -0
  175. evalkit/bundled/ui/assets/java-BEtHBSE6.js +1 -0
  176. evalkit/bundled/ui/assets/javascript-D4EGhYA6.js +1 -0
  177. evalkit/bundled/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  178. evalkit/bundled/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  179. evalkit/bundled/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  180. evalkit/bundled/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  181. evalkit/bundled/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  182. evalkit/bundled/ui/assets/json.worker-CV9fqkUp.js +65 -0
  183. evalkit/bundled/ui/assets/jsonMode-pQjSfIH6.js +7 -0
  184. evalkit/bundled/ui/assets/julia-Bri6UV-V.js +1 -0
  185. evalkit/bundled/ui/assets/kotlin-BOotOW0E.js +1 -0
  186. evalkit/bundled/ui/assets/less-B9JPFI3C.js +2 -0
  187. evalkit/bundled/ui/assets/lexon-CfSJPG6W.js +1 -0
  188. evalkit/bundled/ui/assets/liquid-DbrvoYjD.js +1 -0
  189. evalkit/bundled/ui/assets/lspLanguageFeatures-D0B7cH6y.js +4 -0
  190. evalkit/bundled/ui/assets/lua-CsQS60Ue.js +1 -0
  191. evalkit/bundled/ui/assets/m3-D-oSqn_W.js +1 -0
  192. evalkit/bundled/ui/assets/markdown-Cimd5fb3.js +1 -0
  193. evalkit/bundled/ui/assets/mdx-B0HrujLR.js +1 -0
  194. evalkit/bundled/ui/assets/mips-CIPQ_RoX.js +1 -0
  195. evalkit/bundled/ui/assets/monacoEditor-DsoPncxW.js +68 -0
  196. evalkit/bundled/ui/assets/monacoEditor-n7clI0si.css +1 -0
  197. evalkit/bundled/ui/assets/msdax-DauUninz.js +1 -0
  198. evalkit/bundled/ui/assets/mysql-SOo6toE5.js +1 -0
  199. evalkit/bundled/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  200. evalkit/bundled/ui/assets/pascal-DrH0SRf2.js +1 -0
  201. evalkit/bundled/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  202. evalkit/bundled/ui/assets/perl-oz_6vUea.js +1 -0
  203. evalkit/bundled/ui/assets/pgsql-DTj74zXo.js +1 -0
  204. evalkit/bundled/ui/assets/php-nr791fC2.js +1 -0
  205. evalkit/bundled/ui/assets/pla-CopQ2nXW.js +1 -0
  206. evalkit/bundled/ui/assets/postiats-43DmfD33.js +1 -0
  207. evalkit/bundled/ui/assets/powerquery-D3hlyOfw.js +1 -0
  208. evalkit/bundled/ui/assets/powershell-DmHpPYUd.js +1 -0
  209. evalkit/bundled/ui/assets/protobuf-C531GsRP.js +2 -0
  210. evalkit/bundled/ui/assets/pug-Z5eAx3Zn.js +1 -0
  211. evalkit/bundled/ui/assets/python-CieHVM9n.js +1 -0
  212. evalkit/bundled/ui/assets/qsharp-DkqhCAOL.js +1 -0
  213. evalkit/bundled/ui/assets/r-BwWrilGY.js +1 -0
  214. evalkit/bundled/ui/assets/razor-sLewSMyp.js +1 -0
  215. evalkit/bundled/ui/assets/redis-ClamHrr6.js +1 -0
  216. evalkit/bundled/ui/assets/redshift-DT7zqm-g.js +1 -0
  217. evalkit/bundled/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  218. evalkit/bundled/ui/assets/ruby-DezsRK8O.js +1 -0
  219. evalkit/bundled/ui/assets/rust-DdL9SqIa.js +1 -0
  220. evalkit/bundled/ui/assets/sb-CcwsVR0C.js +1 -0
  221. evalkit/bundled/ui/assets/scala-DHpiXF5c.js +1 -0
  222. evalkit/bundled/ui/assets/scheme-BeGwcela.js +1 -0
  223. evalkit/bundled/ui/assets/scss-gp-XZpBa.js +3 -0
  224. evalkit/bundled/ui/assets/shell-CC2rA5mh.js +1 -0
  225. evalkit/bundled/ui/assets/solidity-BEEn4gHE.js +1 -0
  226. evalkit/bundled/ui/assets/sophia-CRfGWb83.js +1 -0
  227. evalkit/bundled/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  228. evalkit/bundled/ui/assets/sql-NEE52Syq.js +1 -0
  229. evalkit/bundled/ui/assets/st-DbInun42.js +1 -0
  230. evalkit/bundled/ui/assets/swift-Bxkupp3x.js +1 -0
  231. evalkit/bundled/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  232. evalkit/bundled/ui/assets/tcl-DISqw1ZD.js +1 -0
  233. evalkit/bundled/ui/assets/ts.worker-DIAQkoB3.js +67741 -0
  234. evalkit/bundled/ui/assets/tsMode-BcVQ1nu0.js +11 -0
  235. evalkit/bundled/ui/assets/twig-De2hgUGE.js +1 -0
  236. evalkit/bundled/ui/assets/typescript-CUvNWd9p.js +1 -0
  237. evalkit/bundled/ui/assets/typespec-B8J7ngcE.js +1 -0
  238. evalkit/bundled/ui/assets/vb-DV3o63ZY.js +1 -0
  239. evalkit/bundled/ui/assets/wgsl-DpFanUEy.js +298 -0
  240. evalkit/bundled/ui/assets/workers-CwpYQtzp.js +1 -0
  241. evalkit/bundled/ui/assets/xml-C1HnXMdF.js +1 -0
  242. evalkit/bundled/ui/assets/yaml-D7Ey3HQu.js +1 -0
  243. evalkit/bundled/ui/favicon.svg +6 -0
  244. evalkit/bundled/ui/index.html +14 -0
  245. evalkit/cli/__init__.py +0 -0
  246. evalkit/cli/admin_commands.py +590 -0
  247. evalkit/cli/client.py +67 -0
  248. evalkit/cli/init_server.py +165 -0
  249. evalkit/cli/local_run.py +195 -0
  250. evalkit/cli/main.py +1365 -0
  251. evalkit/cli/runs_commands.py +539 -0
  252. evalkit/cli/server_commands.py +130 -0
  253. evalkit/cli/skill_targets.py +43 -0
  254. evalkit/cli/tokens_commands.py +52 -0
  255. evalkit/cli/traces_commands.py +332 -0
  256. evalkit/cli/update.py +85 -0
  257. evalkit/client/__init__.py +0 -0
  258. evalkit/client/agents.py +25 -0
  259. evalkit/client/api.py +193 -0
  260. evalkit/client/blob.py +104 -0
  261. evalkit/client/connection.py +88 -0
  262. evalkit/client/evaluators.py +11 -0
  263. evalkit/client/sessions.py +17 -0
  264. evalkit/client/store.py +139 -0
  265. evalkit/client/summary.py +46 -0
  266. evalkit/client/sync.py +198 -0
  267. evalkit/config.py +227 -0
  268. evalkit/contracts/__init__.py +0 -0
  269. evalkit/contracts/atif.py +115 -0
  270. evalkit/contracts/cases.py +456 -0
  271. evalkit/contracts/custom_evaluator.py +58 -0
  272. evalkit/contracts/enums.py +86 -0
  273. evalkit/contracts/esf.py +120 -0
  274. evalkit/contracts/harness.py +281 -0
  275. evalkit/contracts/notes.json +4 -0
  276. evalkit/contracts/notes.py +7 -0
  277. evalkit/contracts/settings.py +41 -0
  278. evalkit/core/__init__.py +0 -0
  279. evalkit/core/access.py +75 -0
  280. evalkit/core/agents.py +65 -0
  281. evalkit/core/blob.py +46 -0
  282. evalkit/core/capabilities.py +36 -0
  283. evalkit/core/costmap.py +90 -0
  284. evalkit/core/custom_eval.py +50 -0
  285. evalkit/core/data/cost_map.json +81546 -0
  286. evalkit/core/endpoints.py +31 -0
  287. evalkit/core/evaluators/__init__.py +29 -0
  288. evalkit/core/evaluators/base.py +121 -0
  289. evalkit/core/evaluators/gauges.py +84 -0
  290. evalkit/core/evaluators/presentation.py +166 -0
  291. evalkit/core/evaluators/response_time.py +21 -0
  292. evalkit/core/evaluators/rubrics.py +494 -0
  293. evalkit/core/evaluators/tool_trajectory.py +187 -0
  294. evalkit/core/evaluators/trajectory_rubrics.py +198 -0
  295. evalkit/core/fixtures.py +57 -0
  296. evalkit/core/harness.py +227 -0
  297. evalkit/core/harness_check.py +191 -0
  298. evalkit/core/importer.py +257 -0
  299. evalkit/core/judge.py +78 -0
  300. evalkit/core/lint.py +265 -0
  301. evalkit/core/overlays.py +76 -0
  302. evalkit/core/paths.py +25 -0
  303. evalkit/core/promotion.py +94 -0
  304. evalkit/core/pull.py +198 -0
  305. evalkit/core/python_eval.py +59 -0
  306. evalkit/core/remote_evaluators.py +32 -0
  307. evalkit/core/review.py +220 -0
  308. evalkit/core/run_digest.py +421 -0
  309. evalkit/core/runner.py +995 -0
  310. evalkit/core/scorecard.py +255 -0
  311. evalkit/core/traces/__init__.py +0 -0
  312. evalkit/core/traces/convert.py +336 -0
  313. evalkit/core/traces/importer.py +113 -0
  314. evalkit/core/traces/mapping.py +385 -0
  315. evalkit/core/traces/poll.py +38 -0
  316. evalkit/core/traces/raw.py +21 -0
  317. evalkit/core/traces/sessions.py +172 -0
  318. evalkit/core/traces/settle.py +14 -0
  319. evalkit/core/traces/sources/__init__.py +19 -0
  320. evalkit/core/traces/sources/base.py +25 -0
  321. evalkit/core/traces/sources/phoenix.py +121 -0
  322. evalkit/core/trajectory.py +248 -0
  323. evalkit/core/workspace.py +30 -0
  324. evalkit/credentials.py +119 -0
  325. evalkit/db/__init__.py +0 -0
  326. evalkit/db/agents.py +289 -0
  327. evalkit/db/alembic/env.py +90 -0
  328. evalkit/db/alembic/script.py.mako +28 -0
  329. evalkit/db/alembic/versions/0011_baseline.py +1002 -0
  330. evalkit/db/alembic/versions/0012_ticket_token_kind.py +27 -0
  331. evalkit/db/alembic/versions/0013_keys_and_llm_providers.py +67 -0
  332. evalkit/db/cases.py +608 -0
  333. evalkit/db/crypto.py +12 -0
  334. evalkit/db/evaluators.py +156 -0
  335. evalkit/db/experiments.py +363 -0
  336. evalkit/db/fixture_blobs.py +91 -0
  337. evalkit/db/keys.py +78 -0
  338. evalkit/db/llm_providers.py +176 -0
  339. evalkit/db/locks.py +6 -0
  340. evalkit/db/mappings.py +168 -0
  341. evalkit/db/models.py +567 -0
  342. evalkit/db/promotion.py +271 -0
  343. evalkit/db/rekey.py +46 -0
  344. evalkit/db/review.py +250 -0
  345. evalkit/db/review_state.py +50 -0
  346. evalkit/db/runs.py +533 -0
  347. evalkit/db/scorecard.py +300 -0
  348. evalkit/db/session.py +71 -0
  349. evalkit/db/sessions.py +90 -0
  350. evalkit/db/spans.py +360 -0
  351. evalkit/db/store.py +209 -0
  352. evalkit/db/tokens.py +100 -0
  353. evalkit/db/trace_imports.py +308 -0
  354. evalkit/db/trace_sources.py +131 -0
  355. evalkit/db/users.py +186 -0
  356. evalkit/db/workspaces.py +232 -0
  357. evalkit/paths.py +33 -0
  358. evalkit/sdk/__init__.py +29 -0
  359. evalkit/sdk/binding.py +69 -0
  360. evalkit/sdk/evaluators.py +56 -0
  361. evalkit/sdk/runner.py +957 -0
  362. evalkit/sdk/tracing.py +166 -0
  363. evalkit/store/__init__.py +0 -0
  364. evalkit/store/base.py +354 -0
  365. evalkit/store/blob.py +69 -0
  366. evalkit/version.py +44 -0
  367. evalkit_py-0.2.0.dev12.dist-info/METADATA +205 -0
  368. evalkit_py-0.2.0.dev12.dist-info/RECORD +371 -0
  369. evalkit_py-0.2.0.dev12.dist-info/WHEEL +4 -0
  370. evalkit_py-0.2.0.dev12.dist-info/entry_points.txt +2 -0
  371. evalkit_py-0.2.0.dev12.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,539 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import io
5
+ import json
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import zipfile
10
+ from collections.abc import Callable
11
+ from datetime import UTC, datetime, timedelta
12
+ from pathlib import Path
13
+ from urllib.parse import quote
14
+
15
+ import httpx
16
+ import typer
17
+
18
+ from evalkit.cli.client import ApiClient, ApiError
19
+ from evalkit.client.api import encode
20
+ from evalkit.contracts.atif import TrajectoryValidationError
21
+ from evalkit.contracts.harness import RunListItem
22
+ from evalkit.core.agents import AgentDefinition, Driver, driver
23
+ from evalkit.core.harness import HarnessClient
24
+ from evalkit.core.paths import UnsafePathError, is_safe_run_id
25
+ from evalkit.core.pull import (
26
+ MAX_PULL,
27
+ SCAN_NOTE,
28
+ RunBundle,
29
+ bundle_files,
30
+ index_row,
31
+ list_harness_runs,
32
+ merge_index,
33
+ safe_dir,
34
+ )
35
+
36
+ runs_app = typer.Typer(no_args_is_help=True, help="List and pull an agent's runs (app or eval).")
37
+
38
+ RELATIVE = re.compile(r"^(\d+)([dhm])$")
39
+ ISO = re.compile(r"^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?)?(Z|[+-]\d{2}:?\d{2})?$")
40
+ UNITS = {"d": "days", "h": "hours", "m": "minutes"}
41
+ SERVER_PAGE = 200
42
+ DEFAULT_ROOT = Path("evalkit-runs")
43
+ PULL_HELP = (
44
+ "Download runs with their ATIF trajectories into a local directory: index.json plus "
45
+ "runs/<run_id>/ (run.json, trajectory.json, and for eval runs case.json and "
46
+ "evaluations.json).\n\n"
47
+ "Workspace files and full tool outputs are not fetched: the trajectory keeps the "
48
+ "shortened tool output the agent saw. List and fetch a run's files with "
49
+ "evalkit runs files.\n\n"
50
+ "Writes to ./evalkit-runs/… by default. Add that directory to .gitignore, or pass "
51
+ "--out with a path outside the repo. Pulling again into the same --out adds to it.\n\n"
52
+ "Example: evalkit runs pull ouragent@prod --source app --since 7d --limit 25 --offset 0"
53
+ )
54
+
55
+ FILES_HELP = (
56
+ "List a pulled run's workspace files, or fetch some of them. Without --path it prints "
57
+ "the file tree (size and path) and writes it to runs/<run_id>/files.json. Each --path, "
58
+ "a file or a folder for everything under it, is fetched into "
59
+ "runs/<run_id>/files/<path>, one request per file, each written as it arrives; the "
60
+ "first failure stops the command and names the file. A path not in the tree, such as "
61
+ "a tool result's extra.full_output_path from trajectory.json, is fetched directly.\n\n"
62
+ "Example: evalkit runs files evalkit-runs/ouragent@prod/20261023-101500 RUN_ID "
63
+ "--path outputs/report.md"
64
+ )
65
+
66
+ LocalHarness = Callable[[AgentDefinition], HarnessClient]
67
+
68
+
69
+ def parse_when(text: str | None, now: datetime | None = None) -> datetime | None:
70
+ if text is None:
71
+ return None
72
+ match = RELATIVE.match(text)
73
+ if match:
74
+ return (now or datetime.now(UTC)) - timedelta(**{UNITS[match[2]]: int(match[1])})
75
+ if not ISO.match(text):
76
+ raise ApiError(f"not a time: {text!r}; use ISO 8601 (2026-10-08T00:00Z) or 7d / 12h / 30m")
77
+ parsed = datetime.fromisoformat(text)
78
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
79
+
80
+
81
+ def _iso(value: datetime | None) -> str | None:
82
+ return value.isoformat() if value else None
83
+
84
+
85
+ def _list(client: ApiClient, agent: str, params: dict) -> dict:
86
+ query = {k: v for k, v in params.items() if v is not None}
87
+ return client.get(f"/api/agents/{encode(agent)}/runs", params=query).json()
88
+
89
+
90
+ def _list_stored(client: ApiClient, agent: str, filters: dict, wanted: int) -> tuple[list, int]:
91
+ rows: list[dict] = []
92
+ total = 0
93
+ while len(rows) < wanted:
94
+ size = min(SERVER_PAGE, wanted - len(rows))
95
+ body = _list(client, agent, {**filters, "limit": size, "offset": len(rows)})
96
+ rows += body["runs"]
97
+ total = body["total"]
98
+ if len(body["runs"]) < size:
99
+ break
100
+ return rows, total
101
+
102
+
103
+ def _known(client: ApiClient, agent: str, run_ids: list[str]) -> set[str]:
104
+ known: set[str] = set()
105
+ for start in range(0, len(run_ids), MAX_PULL):
106
+ chunk = run_ids[start : start + MAX_PULL]
107
+ body = client.get(
108
+ f"/api/agents/{encode(agent)}/runs", params=[("run_id", i) for i in chunk]
109
+ ).json()
110
+ known |= {row["run_id"] for row in body["runs"]}
111
+ return known
112
+
113
+
114
+ def _unzip(content: bytes) -> tuple[dict, dict[str, bytes]]:
115
+ archive = zipfile.ZipFile(io.BytesIO(content))
116
+ files = {m.filename: archive.read(m) for m in archive.infolist() if not m.is_dir()}
117
+ return json.loads(files.pop("index.json")), files
118
+
119
+
120
+ def _server_pull_ids(client: ApiClient, agent: str, run_ids: list[str]) -> tuple[list, dict]:
121
+ rows: list[dict] = []
122
+ files: dict[str, bytes] = {}
123
+ for start in range(0, len(run_ids), MAX_PULL):
124
+ chunk = run_ids[start : start + MAX_PULL]
125
+ resp = client.post(f"/api/agents/{encode(agent)}/runs/pull", json={"run_ids": chunk})
126
+ index, chunk_files = _unzip(resp.content)
127
+ rows += index["runs"]
128
+ files |= chunk_files
129
+ return rows, files
130
+
131
+
132
+ def write_pull(target: Path, index: dict, files: dict[str, bytes]) -> None:
133
+ target = write_files(target, files)
134
+ existing_path = target / "index.json"
135
+ existing = json.loads(existing_path.read_text()) if existing_path.is_file() else None
136
+ existing_path.write_text(json.dumps(merge_index(existing, index), indent=2) + "\n")
137
+
138
+
139
+ def write_files(target: Path, files: dict[str, bytes]) -> Path:
140
+ target = target.resolve()
141
+ target.mkdir(parents=True, exist_ok=True)
142
+ # Member names carry run ids and harness-reported paths — validate each one
143
+ # resolves under the destination before writing.
144
+ for name, data in files.items():
145
+ destination = (target / name).resolve()
146
+ if not destination.is_relative_to(target):
147
+ raise ApiError(f"archive member escapes the output directory: {name!r}")
148
+ destination.parent.mkdir(parents=True, exist_ok=True)
149
+ destination.write_bytes(data)
150
+ return target
151
+
152
+
153
+ def _stamp(target: Path, filters: dict, total: int | None = None) -> None:
154
+ path = target.resolve() / "index.json"
155
+ index = json.loads(path.read_text())
156
+ index["filters"] = filters
157
+ if total is not None:
158
+ index["total_matching"] = total
159
+ path.write_text(json.dumps(index, indent=2) + "\n")
160
+
161
+
162
+ def unignored_in_git(target: Path) -> bool:
163
+ if shutil.which("git") is None:
164
+ return False
165
+ probe = target if target.exists() else target.parent
166
+ while not probe.exists():
167
+ probe = probe.parent
168
+ inside = subprocess.run(
169
+ ["git", "-C", str(probe), "rev-parse", "--is-inside-work-tree"],
170
+ capture_output=True,
171
+ text=True,
172
+ )
173
+ if inside.returncode != 0 or inside.stdout.strip() != "true":
174
+ return False
175
+ ignored = subprocess.run(
176
+ ["git", "-C", str(probe), "check-ignore", "-q", str(target.resolve())],
177
+ capture_output=True,
178
+ )
179
+ return ignored.returncode != 0
180
+
181
+
182
+ async def _harness_rows(
183
+ harness: HarnessClient,
184
+ known: Callable[[list[str]], set[str]],
185
+ since: datetime | None,
186
+ until: datetime | None,
187
+ wanted: int,
188
+ ) -> tuple[list[RunListItem], int, str | None]:
189
+ async def exclude(run_ids: list[str]) -> set[str]:
190
+ return known(run_ids)
191
+
192
+ try:
193
+ listed = await list_harness_runs(
194
+ harness, wanted=wanted, since=since, until=until, exclude=exclude
195
+ )
196
+ finally:
197
+ await harness.aclose()
198
+ return listed.runs, listed.total, SCAN_NOTE if listed.capped else None
199
+
200
+
201
+ def _require_run_id(run_id: str) -> None:
202
+ if not is_safe_run_id(run_id):
203
+ raise ApiError(f"run id {run_id!r} must be one plain path segment")
204
+
205
+
206
+ async def _harness_bundle(harness: HarnessClient, row: dict) -> RunBundle:
207
+ run_id = row["run_id"]
208
+ bundle = RunBundle(
209
+ run_id=run_id,
210
+ source="app",
211
+ created_at=row.get("created_at"),
212
+ title=row.get("title"),
213
+ run={"run_id": run_id, "source": "app"},
214
+ )
215
+
216
+ try:
217
+ bundle.trajectory = (await harness.trajectory(run_id)).model_dump(exclude_none=True)
218
+ except (httpx.HTTPError, TrajectoryValidationError, UnsafePathError) as exc:
219
+ bundle.pull_error = str(exc)
220
+ return bundle
221
+
222
+
223
+ async def _harness_bundles(harness: HarnessClient, rows: list[dict]) -> list[RunBundle]:
224
+ bundles = [await _harness_bundle(harness, row) for row in rows]
225
+ await harness.aclose()
226
+ return bundles
227
+
228
+
229
+ def _sort_key(row: dict) -> datetime:
230
+ created = row.get("created_at")
231
+ if not created:
232
+ return datetime.min.replace(tzinfo=UTC)
233
+ parsed = datetime.fromisoformat(created)
234
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
235
+
236
+
237
+ def _local_rows(
238
+ client: ApiClient,
239
+ definition: AgentDefinition,
240
+ harness: LocalHarness,
241
+ filters: dict,
242
+ limit: int,
243
+ offset: int,
244
+ ) -> tuple[list[dict], int, str | None]:
245
+ agent = definition.ref
246
+ stored, stored_total = _list_stored(client, agent, filters, limit + offset)
247
+ extra: list[dict] = []
248
+ extra_total = 0
249
+ note: str | None = None
250
+ if filters["source"] != "eval":
251
+ try:
252
+ listed, extra_total, note = asyncio.run(
253
+ _harness_rows(
254
+ harness(definition),
255
+ lambda run_ids: _known(client, agent, run_ids),
256
+ parse_when(filters["since"]),
257
+ parse_when(filters["until"]),
258
+ limit + offset,
259
+ )
260
+ )
261
+ except httpx.HTTPError as exc:
262
+ listed, note = [], f"harness unreachable: {exc}"
263
+ extra = [
264
+ {**run.model_dump(mode="json"), "source": "app", "stored": False} for run in listed
265
+ ]
266
+ rows = sorted([*stored, *extra], key=_sort_key, reverse=True)
267
+ return rows[offset : offset + limit], stored_total + extra_total, note
268
+
269
+
270
+ def _pull_local(
271
+ client: ApiClient,
272
+ definition: AgentDefinition,
273
+ harness: LocalHarness,
274
+ rows: list[dict],
275
+ ) -> tuple[dict, dict[str, bytes]]:
276
+ agent = definition.ref
277
+ known = _known(client, agent, [row["run_id"] for row in rows])
278
+ index_rows, files = _server_pull_ids(
279
+ client, agent, [row["run_id"] for row in rows if row["run_id"] in known]
280
+ )
281
+ local = asyncio.run(
282
+ _harness_bundles(harness(definition), [r for r in rows if r["run_id"] not in known])
283
+ )
284
+ for bundle in local:
285
+ files |= bundle_files(bundle)
286
+ return {
287
+ "agent": agent,
288
+ "pulled_at": datetime.now(UTC).isoformat(),
289
+ "runs": index_rows + [index_row(bundle) for bundle in local],
290
+ }, files
291
+
292
+
293
+ def register(
294
+ app: typer.Typer,
295
+ client_factory: Callable[[], ApiClient],
296
+ agent_definition: Callable[[str], AgentDefinition],
297
+ local_harness: LocalHarness,
298
+ ) -> None:
299
+ app.add_typer(runs_app, name="runs")
300
+
301
+ @runs_app.command("list")
302
+ def runs_list(
303
+ agent: str = typer.Argument(..., help="myagent (yours) or ouragent@dev."),
304
+ source: str = typer.Option("all", "--source", help="app, eval or all."),
305
+ since: str | None = typer.Option(None, "--since", help="ISO time, or 7d / 12h / 30m."),
306
+ until: str | None = typer.Option(None, "--until", help="ISO time, or 7d / 12h / 30m."),
307
+ limit: int = typer.Option(50, "--limit"),
308
+ offset: int = typer.Option(0, "--offset"),
309
+ as_json: bool = typer.Option(False, "--json", help="One JSON object per line."),
310
+ ):
311
+ """List an agent's runs: app runs (outside any experiment) and eval runs.
312
+
313
+ Example: evalkit runs list ouragent@prod --source app --since 7d
314
+ """
315
+ client = client_factory()
316
+ definition = agent_definition(agent)
317
+ filters = _filters(source, parse_when(since), parse_when(until))
318
+ note = None
319
+ if driver(definition) is Driver.CLI:
320
+ rows, total, note = _local_rows(
321
+ client, definition, local_harness, filters, limit, offset
322
+ )
323
+ else:
324
+ body = _list(client, definition.ref, {**filters, "limit": limit, "offset": offset})
325
+ rows, total, note = body["runs"], body["total"], body.get("harness_note")
326
+ if driver(definition) is Driver.SDK and not definition.is_otel and source != "eval":
327
+ note = (
328
+ f"{definition.ref} has no harness and no traces, so evalkit only sees its eval runs"
329
+ )
330
+ if as_json:
331
+ for row in rows:
332
+ typer.echo(json.dumps(row))
333
+ typer.echo(f"{len(rows)} of {total} run(s)", err=True)
334
+ if note:
335
+ typer.echo(f"note: {note}", err=True)
336
+ return
337
+ for row in rows:
338
+ where = (
339
+ f"exp {row['experiment_id']} {row['case_id']}" if row.get("experiment_id") else ""
340
+ )
341
+ typer.echo(
342
+ f"{row['run_id']:<38} {row['source']:<4} {row.get('status') or '-':<10} "
343
+ f"{row.get('created_at') or '-':<26} {where} {row.get('title') or ''}".rstrip()
344
+ )
345
+ typer.echo(f"{len(rows)} of {total} run(s)")
346
+ if note:
347
+ typer.echo(f"note: {note}")
348
+
349
+ @runs_app.command("pull", help=PULL_HELP)
350
+ def runs_pull(
351
+ agent: str | None = typer.Argument(None, help="myagent (yours) or ouragent@dev."),
352
+ source: str = typer.Option("all", "--source", help="app, eval or all."),
353
+ since: str | None = typer.Option(None, "--since", help="ISO time, or 7d / 12h / 30m."),
354
+ until: str | None = typer.Option(None, "--until", help="ISO time, or 7d / 12h / 30m."),
355
+ limit: int | None = typer.Option(
356
+ None,
357
+ "--limit",
358
+ help=f"Runs to pull; default {MAX_PULL}. Above {MAX_PULL} on purpose only.",
359
+ ),
360
+ offset: int = typer.Option(0, "--offset", help="Skip this many matching runs."),
361
+ run_id: list[str] = typer.Option([], "--run-id", help="Pull these runs only."),
362
+ experiment: int | None = typer.Option(None, "--experiment", help="Pull every run of it."),
363
+ out: Path | None = typer.Option(None, "--out", help="Output directory."),
364
+ ):
365
+ client = client_factory()
366
+ if experiment is not None:
367
+ ref = client.get(f"/api/experiments/{experiment}").json()["agent"]
368
+ target = out or DEFAULT_ROOT / f"experiment-{experiment}"
369
+ pulled = 0
370
+ while True:
371
+ resp = client.post(
372
+ f"/api/agents/{encode(ref)}/runs/pull",
373
+ json={
374
+ "experiment_id": experiment,
375
+ "limit": MAX_PULL,
376
+ "offset": pulled,
377
+ },
378
+ )
379
+ index, members = _unzip(resp.content)
380
+ write_pull(target, index, members)
381
+ pulled += len(index["runs"])
382
+ if not index["runs"] or pulled >= index["total_matching"]:
383
+ break
384
+ _stamp(target, {"experiment": experiment})
385
+ _report(target)
386
+ return
387
+ if agent is None:
388
+ raise ApiError("pass an agent, or --experiment ID")
389
+ definition = agent_definition(agent)
390
+ ref = definition.ref
391
+ start, end = parse_when(since), parse_when(until)
392
+ filters = _filters(source, start, end)
393
+ wanted = limit if limit is not None else MAX_PULL
394
+ target = out or DEFAULT_ROOT / ref / datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
395
+ local = driver(definition) is Driver.CLI
396
+ total: int | None = None
397
+ if run_id:
398
+ total = len(run_id)
399
+ if run_id and local:
400
+ rows = [{"run_id": i} for i in run_id]
401
+ write_pull(target, *_pull_local(client, definition, local_harness, rows))
402
+ elif run_id:
403
+ index_rows, members = _server_pull_ids(client, ref, run_id)
404
+ write_pull(
405
+ target,
406
+ {"agent": ref, "pulled_at": datetime.now(UTC).isoformat(), "runs": index_rows},
407
+ members,
408
+ )
409
+ elif local:
410
+ rows, total, note = _local_rows(
411
+ client, definition, local_harness, filters, wanted, offset
412
+ )
413
+ local_index, members = _pull_local(client, definition, local_harness, rows)
414
+ write_pull(target, {**local_index, "note": note}, members)
415
+ else:
416
+ done = 0
417
+ while done < wanted:
418
+ size = min(MAX_PULL, wanted - done)
419
+ resp = client.post(
420
+ f"/api/agents/{encode(ref)}/runs/pull",
421
+ json={**filters, "limit": size, "offset": offset + done},
422
+ )
423
+ index, members = _unzip(resp.content)
424
+ write_pull(target, index, members)
425
+ total = index["total_matching"]
426
+ done += len(index["runs"])
427
+ if len(index["runs"]) < size:
428
+ break
429
+ invocation = {
430
+ **filters,
431
+ "limit": wanted,
432
+ "offset": offset,
433
+ "run_ids": run_id or None,
434
+ }
435
+ _stamp(target, invocation, total)
436
+ _report(target)
437
+
438
+ @runs_app.command("files", help=FILES_HELP)
439
+ def runs_files(
440
+ directory: Path = typer.Argument(..., help="A directory evalkit runs pull wrote."),
441
+ run_id: str = typer.Argument(..., help="A run in its index.json."),
442
+ path: list[str] = typer.Option(
443
+ [], "--path", help="A file, or a folder for everything under it. Repeatable."
444
+ ),
445
+ ):
446
+ index_path = directory / "index.json"
447
+ if not index_path.is_file():
448
+ raise ApiError(f"{directory} has no index.json; pull runs into it first")
449
+ index = json.loads(index_path.read_text())
450
+ if run_id not in {row["run_id"] for row in index["runs"]}:
451
+ raise ApiError(f"{run_id} is not in {index_path}; pull it first")
452
+ client = client_factory()
453
+ definition = agent_definition(index["agent"])
454
+ files_url = f"/api/agents/{encode(definition.ref)}/runs/{encode(run_id)}/files"
455
+ listing = client.get(files_url).json()["files"]
456
+ stored = {entry["path"] for entry in listing if entry.get("stored")}
457
+ personal = driver(definition) is Driver.CLI
458
+ if personal and not listing:
459
+ listing = asyncio.run(_harness_listing(local_harness(definition), run_id))
460
+ base = f"runs/{safe_dir(run_id)}"
461
+ tree = json.dumps({"files": listing}, indent=2) + "\n"
462
+ write_files(directory, {f"{base}/files.json": tree.encode()})
463
+ if not path:
464
+ for entry in listing:
465
+ size = "-" if entry.get("size") is None else entry["size"]
466
+ typer.echo(f"{size:>12} {entry['path']}")
467
+ typer.echo(f"{len(listing)} file(s); fetch with --path <file or folder>")
468
+ return
469
+
470
+ def save(name: str, data: bytes) -> None:
471
+ write_files(directory, {f"{base}/files/{name}": data})
472
+
473
+ picked = _picked(listing, path)
474
+ from_server = [p for p in picked if p in stored or not personal]
475
+ for name in from_server:
476
+ typer.echo(f"fetching {name}")
477
+ save(name, client.get(f"{files_url}/{quote(name, safe='/')}").content)
478
+ from_harness = [p for p in picked if p not in from_server]
479
+ if from_harness:
480
+ asyncio.run(_harness_fetch(local_harness(definition), run_id, from_harness, save))
481
+ target = (directory / base / "files").resolve()
482
+ typer.echo(f"fetched {len(picked)} file(s) to {target}")
483
+
484
+
485
+ def _filters(source: str, since: datetime | None, until: datetime | None) -> dict:
486
+ return {"source": source, "since": _iso(since), "until": _iso(until)}
487
+
488
+
489
+ def _report(target: Path) -> None:
490
+ index = json.loads((target / "index.json").read_text())
491
+ failed = sum(1 for row in index["runs"] if row.get("pull_error"))
492
+ typer.echo(f"pulled {len(index['runs'])} run(s) to {target.resolve()}")
493
+ if index.get("note"):
494
+ typer.echo(f"note: {index['note']}")
495
+ if failed:
496
+ typer.echo(f"{failed} run(s) could not be fetched in full; see pull_error in run.json")
497
+ if unignored_in_git(target):
498
+ typer.secho(
499
+ f"warning: {target} is inside a git repo and not ignored; add it to .gitignore",
500
+ fg="yellow",
501
+ err=True,
502
+ )
503
+
504
+
505
+ def _picked(listing: list[dict], wanted: list[str]) -> list[str]:
506
+ paths = [entry["path"] for entry in listing]
507
+ picked: list[str] = []
508
+ for want in wanted:
509
+ prefix = want.strip("/")
510
+ if not prefix and not paths:
511
+ raise ApiError("this run has no files to fetch")
512
+ matched = [p for p in paths if not prefix or p == prefix or p.startswith(f"{prefix}/")]
513
+ picked += [p for p in matched or [prefix] if p not in picked]
514
+ return picked
515
+
516
+
517
+ async def _harness_listing(harness: HarnessClient, run_id: str) -> list[dict]:
518
+ _require_run_id(run_id)
519
+ try:
520
+ if not (await harness.capabilities()).provides.workspace:
521
+ raise ApiError("this harness does not expose workspace files")
522
+ return (await harness.list_files(run_id)).model_dump()["files"]
523
+ finally:
524
+ await harness.aclose()
525
+
526
+
527
+ async def _harness_fetch(
528
+ harness: HarnessClient, run_id: str, paths: list[str], save: Callable[[str, bytes], None]
529
+ ) -> None:
530
+ _require_run_id(run_id)
531
+ try:
532
+ for name in paths:
533
+ typer.echo(f"fetching {name}")
534
+ data = await harness.find_file(run_id, name)
535
+ if data is None:
536
+ raise ApiError(f"{name}: no such file on the harness")
537
+ save(name, data)
538
+ finally:
539
+ await harness.aclose()
@@ -0,0 +1,130 @@
1
+ """Server-side CLI command bodies.
2
+
3
+ Everything here needs the `evalkit-py[server]` extra (uvicorn, FastAPI, SQLAlchemy,
4
+ alembic). The base CLI install stays a thin HTTP client; `main.py` imports this
5
+ module only when the server extra is present and points users at it otherwise.
6
+ """
7
+
8
+ import asyncio
9
+
10
+ import psycopg
11
+ import sentry_sdk
12
+ import uvicorn
13
+ from cryptography.fernet import Fernet
14
+ from psycopg import sql
15
+ from sentry_sdk.integrations.fastapi import FastApiIntegration
16
+ from sentry_sdk.integrations.starlette import StarletteIntegration
17
+ from sqlalchemy.engine import make_url
18
+
19
+ from evalkit import version
20
+ from evalkit.api.app import create_app
21
+ from evalkit.cli.client import ApiError
22
+ from evalkit.config import Config
23
+ from evalkit.contracts.settings import JudgeSettings
24
+ from evalkit.db.crypto import SecretBox
25
+ from evalkit.db.llm_providers import LlmProviderStore
26
+ from evalkit.db.rekey import rekey_all
27
+ from evalkit.db.session import make_session_factory, run_migrations
28
+
29
+
30
+ def init_sentry(config: Config) -> None:
31
+ if config.sentry is None:
32
+ return
33
+ sentry_sdk.init(
34
+ dsn=config.sentry.dsn,
35
+ environment=config.sentry.environment,
36
+ release=f"evalkit@{version.current()}",
37
+ traces_sample_rate=config.sentry.traces_sample_rate,
38
+ send_default_pii=False,
39
+ max_request_body_size=config.sentry.max_request_body_size,
40
+ integrations=[
41
+ StarletteIntegration(failed_request_status_codes={500}),
42
+ FastApiIntegration(failed_request_status_codes={500}),
43
+ ],
44
+ )
45
+
46
+
47
+ def serve(config: Config, host: str, port: int) -> None:
48
+ init_sentry(config)
49
+ uvicorn.run(create_app(config), host=host, port=port)
50
+
51
+
52
+ def db_upgrade(config: Config, revision: str) -> None:
53
+ run_migrations(config.db.url, revision)
54
+
55
+
56
+ async def rekey(config: Config, old_key: str, new_key: str) -> int:
57
+ factory = make_session_factory(config.db.url)
58
+ count = await rekey_all(factory, SecretBox(old_key), SecretBox(new_key))
59
+ await factory.kw["bind"].dispose()
60
+ return count
61
+
62
+
63
+ def create_database_if_missing(url: str) -> bool:
64
+ parsed = make_url(url).set(drivername="postgresql")
65
+ if not parsed.database:
66
+ raise ValueError(f"{url} names no database")
67
+ try:
68
+ psycopg.connect(parsed.render_as_string(hide_password=False)).close()
69
+ return False
70
+ except psycopg.OperationalError as exc:
71
+ if f'database "{parsed.database}" does not exist' not in str(exc):
72
+ raise ApiError(
73
+ f"could not connect to Postgres at {parsed.host or 'localhost'}:"
74
+ f"{parsed.port or 5432}: {str(exc).strip()}\n"
75
+ "Is Postgres running (brew services start postgresql@17, sudo systemctl "
76
+ "start postgresql, Postgres.app), and are the user and password in the URL "
77
+ "right? See Try it locally > When something fails."
78
+ ) from exc
79
+ admin = parsed.set(database="postgres").render_as_string(hide_password=False)
80
+ with psycopg.connect(admin, autocommit=True) as conn:
81
+ conn.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(parsed.database)))
82
+ return True
83
+
84
+
85
+ def init_database(url: str) -> bool:
86
+ created = create_database_if_missing(url)
87
+ run_migrations(url, "head")
88
+ return created
89
+
90
+
91
+ TRY_PROVIDER = "try"
92
+
93
+
94
+ def new_encryption_key() -> str:
95
+ return Fernet.generate_key().decode()
96
+
97
+
98
+ async def _has_llm_setup(url: str) -> bool:
99
+ factory = make_session_factory(url)
100
+ store = LlmProviderStore(factory)
101
+ providers = await store.list_providers(include_archived=True)
102
+ judge = await store.default_judge()
103
+ await factory.kw["bind"].dispose()
104
+ return bool(providers) or judge is not None
105
+
106
+
107
+ def has_llm_setup(url: str) -> bool:
108
+ return asyncio.run(_has_llm_setup(url))
109
+
110
+
111
+ async def _register_default_judge(
112
+ url: str, encryption_key: str, base_url: str, api_key: str, model: str
113
+ ) -> None:
114
+ factory = make_session_factory(url)
115
+ store = LlmProviderStore(factory)
116
+ sealed = SecretBox(encryption_key).seal(api_key)
117
+ existing = await store.active_by_base_url(base_url)
118
+ if existing is None:
119
+ await store.create(TRY_PROVIDER, base_url, sealed)
120
+ else:
121
+ await store.edit(existing.name, api_key_encrypted=sealed)
122
+ judge = JudgeSettings.model_validate({"base_url": base_url, "model": model, "concurrency": 4})
123
+ await store.set_default_judge(judge)
124
+ await factory.kw["bind"].dispose()
125
+
126
+
127
+ def register_default_judge(
128
+ url: str, encryption_key: str, base_url: str, api_key: str, model: str
129
+ ) -> None:
130
+ asyncio.run(_register_default_judge(url, encryption_key, base_url, api_key, model))