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/cli/main.py ADDED
@@ -0,0 +1,1365 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import time
7
+ from datetime import UTC, datetime
8
+ from importlib.util import find_spec
9
+ from pathlib import Path
10
+
11
+ import httpx
12
+ import typer
13
+ from dotenv import load_dotenv
14
+
15
+ from evalkit import version
16
+ from evalkit.cli import init_server
17
+ from evalkit.cli.admin_commands import register as register_admin
18
+ from evalkit.cli.client import ApiClient, ApiError
19
+ from evalkit.cli.local_run import (
20
+ check_local,
21
+ evaluate_local,
22
+ local_input_schema,
23
+ mark_interrupted,
24
+ run_local,
25
+ )
26
+ from evalkit.cli.local_run import (
27
+ credentials as local_credentials,
28
+ )
29
+ from evalkit.cli.runs_commands import register as register_runs
30
+ from evalkit.cli.skill_targets import INSTALL_MANIFEST
31
+ from evalkit.cli.skill_targets import record as record_skill_target
32
+ from evalkit.cli.tokens_commands import register as register_tokens
33
+ from evalkit.cli.traces_commands import register as register_traces
34
+ from evalkit.cli.update import run_update
35
+ from evalkit.client import connection
36
+ from evalkit.client.agents import definition_of
37
+ from evalkit.client.api import ApiRequestError, VersionMismatch, encode
38
+ from evalkit.client.summary import summary_lines
39
+ from evalkit.client.sync import git_revision, resolve_path, sync_dataset
40
+ from evalkit.config import Config, LocalConfig, MissingEnvError, load_config
41
+ from evalkit.contracts.cases import REGISTRY_FILENAME, is_case_yaml, load_cases_from_yaml
42
+ from evalkit.contracts.settings import JudgeSettings
43
+ from evalkit.core.agents import AgentDefinition, Driver, driver, explicit, server_may_drive
44
+ from evalkit.core.fixtures import load_registries
45
+ from evalkit.core.harness import HarnessClient
46
+ from evalkit.core.lint import fixture_findings
47
+ from evalkit.core.lint import lint as lint_cases
48
+ from evalkit.core.review import CaseState
49
+ from evalkit.core.runner import ScoringFailed, dropped_cases_text
50
+ from evalkit.core.workspace import Reference, parse_reference, slug
51
+ from evalkit.credentials import MissingCredential
52
+ from evalkit.paths import contracts_root, docs_root, skills_root
53
+ from evalkit.store.base import GitRevision, Selection
54
+
55
+ # `serve` and `db upgrade` need the `evalkit-py[server]` extra; the base install
56
+ # is a thin HTTP client. The boundary is explicit here rather than hidden in
57
+ # per-command imports.
58
+ if find_spec("uvicorn") and find_spec("sqlalchemy"):
59
+ from evalkit.cli import server_commands
60
+ else:
61
+ server_commands = None # type: ignore[assignment]
62
+
63
+ RUN_POLL_SECONDS = 2.0
64
+
65
+ app = typer.Typer(
66
+ name="evalkit",
67
+ no_args_is_help=True,
68
+ epilog="Bugs, feature requests and feedback: https://github.com/inferedge-ai/evalkit/issues",
69
+ )
70
+ datasets_app = typer.Typer(no_args_is_help=True, help="List datasets and their cases.")
71
+ harness_app = typer.Typer(no_args_is_help=True, help="Verify a client's Harness API.")
72
+ users_app = typer.Typer(no_args_is_help=True, help="Manage server users (admin only).")
73
+ skills_app = typer.Typer(no_args_is_help=True, help="Install the bundled agent skills.")
74
+ contracts_app = typer.Typer(no_args_is_help=True, help="Export the bundled API contracts.")
75
+ db_app = typer.Typer(no_args_is_help=True, help="Server database schema upgrades (server only).")
76
+ review_app = typer.Typer(no_args_is_help=True, help="Request, give and read case reviews.")
77
+ app.add_typer(datasets_app, name="datasets")
78
+ app.add_typer(harness_app, name="harness")
79
+ app.add_typer(users_app, name="users")
80
+ app.add_typer(skills_app, name="skills")
81
+ app.add_typer(contracts_app, name="contracts")
82
+ app.add_typer(db_app, name="db")
83
+ app.add_typer(review_app, name="review")
84
+
85
+
86
+ def _print_version(value: bool) -> None:
87
+ if value:
88
+ typer.echo(version.current())
89
+ raise typer.Exit()
90
+
91
+
92
+ @app.callback()
93
+ def main(
94
+ show_version: bool = typer.Option(
95
+ False, "--version", callback=_print_version, is_eager=True, help="Print the CLI version."
96
+ ),
97
+ ) -> None:
98
+ """evalkit — self-hosted agent evals. Runs execute here against your local agent;
99
+ results land on the server."""
100
+ logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s")
101
+
102
+
103
+ def _active_connection() -> connection.Connection:
104
+ try:
105
+ return connection.active()
106
+ except LookupError as exc:
107
+ raise ApiError(str(exc)) from exc
108
+
109
+
110
+ def _client() -> ApiClient:
111
+ active = _active_connection()
112
+ return ApiClient(active.url, active.token)
113
+
114
+
115
+ def _connection() -> connection.Connection:
116
+ active = _active_connection()
117
+ me = _client().get("/api/auth/me").json()
118
+ warning = connection.token_expiry_warning(active, me.get("token_expires_at"), datetime.now(UTC))
119
+ if warning is not None:
120
+ typer.secho(warning, fg="yellow", err=True)
121
+ return active._replace(username=me["username"])
122
+
123
+
124
+ def _agent_slug(text: str) -> str:
125
+ return slug(_parse(text, _connection().username or ""))
126
+
127
+
128
+ register_tokens(app, _client)
129
+ register_admin(app, _client, lambda: _connection().username or "")
130
+ register_traces(app, _client, _agent_slug)
131
+ register_runs(
132
+ app,
133
+ lambda: _client(),
134
+ lambda text: _agent_definition(_parse(text, _connection().username or "")),
135
+ lambda definition: HarnessClient(
136
+ definition,
137
+ local_credentials(_with_secrets(_local_config(), scoring=False), _connection()).key_for(
138
+ "agent", definition.ref
139
+ ),
140
+ ),
141
+ )
142
+
143
+
144
+ def _parse(text: str, workspace: str) -> Reference:
145
+ try:
146
+ return parse_reference(text, workspace)
147
+ except ValueError as exc:
148
+ raise ApiError(str(exc)) from exc
149
+
150
+
151
+ def _resolve(arg: str, cases_root: Path | None, workspace: str):
152
+ try:
153
+ return resolve_path(arg, cases_root, workspace)
154
+ except ValueError as exc:
155
+ raise ApiError(str(exc)) from exc
156
+
157
+
158
+ def _find_local() -> LocalConfig | None:
159
+ try:
160
+ return LocalConfig.find(Path.cwd())
161
+ except ValueError as exc:
162
+ raise ApiError(str(exc)) from exc
163
+
164
+
165
+ def _local_config() -> LocalConfig:
166
+ local = _find_local()
167
+ if local is None:
168
+ raise ApiError("no evalkit.yaml found above the current directory")
169
+ return local
170
+
171
+
172
+ def _with_secrets(local: LocalConfig, *, scoring: bool) -> LocalConfig:
173
+ try:
174
+ return local.resolve_env(scoring=scoring)
175
+ except MissingEnvError as exc:
176
+ raise ApiError(str(exc)) from exc
177
+
178
+
179
+ def _server_config(path: Path) -> Config:
180
+ load_dotenv(path.parent / ".env", override=False)
181
+ try:
182
+ return load_config(path)
183
+ except ValueError as exc:
184
+ raise ApiError(str(exc)) from exc
185
+
186
+
187
+ def _relative_files(
188
+ path: Path, patterns: tuple[str, ...], *, allow_empty: bool = False
189
+ ) -> list[tuple[str, Path]]:
190
+ if path.is_dir():
191
+ matched = sorted(
192
+ f
193
+ for pattern in patterns
194
+ for f in path.rglob(pattern)
195
+ if f.is_file() and not any(part.startswith(".") for part in f.relative_to(path).parts)
196
+ )
197
+ files = [(f.relative_to(path).as_posix(), f) for f in matched]
198
+ else:
199
+ files = [(path.name, path)]
200
+ if not allow_empty and not any(name.endswith((".yaml", ".yml")) for name, _ in files):
201
+ raise ApiError(f"no YAML files under {path}")
202
+ return files
203
+
204
+
205
+ def _yaml_files(path: Path, *, allow_empty: bool = False) -> list[tuple[str, Path]]:
206
+ files = _relative_files(path, ("*.yaml", "*.yml"), allow_empty=allow_empty)
207
+ if path.is_dir():
208
+ files = [(name, p) for name, p in files if is_case_yaml(p.relative_to(path))]
209
+ return files
210
+
211
+
212
+ def _nearest_registry(path: Path) -> Path | None:
213
+ start = path if path.is_dir() else path.parent
214
+ for directory in (start, *start.parents):
215
+ candidate = directory / REGISTRY_FILENAME
216
+ if candidate.is_file():
217
+ return candidate
218
+ return None
219
+
220
+
221
+ def _print_findings(findings: list[dict]) -> None:
222
+ for f in findings:
223
+ marker = "✗" if f["severity"] == "error" else "⚠"
224
+ typer.echo(f" {marker} {f['code']} [{f['case_id']}] {f['message']}")
225
+
226
+
227
+ @app.command()
228
+ def login(
229
+ url: str = typer.Option(None, "--url", help="Server base URL, e.g. https://evalkit.internal"),
230
+ username: str = typer.Option(None, "--username"),
231
+ password: str = typer.Option(None, "--password"),
232
+ ):
233
+ """Log in to a server and store the login in ~/.evalkit/config.toml, replacing any
234
+ earlier one.
235
+
236
+ Example: evalkit login --url https://evalkit.example.com
237
+ """
238
+ url = url or typer.prompt("Server URL")
239
+ username = username or typer.prompt("Username")
240
+ password = password or typer.prompt("Password", hide_input=True)
241
+ body = (
242
+ ApiClient(url)
243
+ .post("/api/auth/login", json={"username": username, "password": password})
244
+ .json()
245
+ )
246
+ path = connection.save(connection.Connection(url, body["token"], body["username"]))
247
+ typer.echo(f"logged in as {username} on {url}; saved to {path}")
248
+
249
+
250
+ @app.command()
251
+ def whoami():
252
+ """Check the stored token: who it logs in as and when it expires.
253
+
254
+ Exits 1 when the server rejects the token. Run it before a long run or
255
+ before handing the token to an exporter. Example: evalkit whoami
256
+ """
257
+ active = _active_connection()
258
+ me = _client().get("/api/auth/me").json()
259
+ source = " (from EVALKIT_SERVER_URL)" if active.from_env else ""
260
+ typer.echo(f"{me['username']} on {active.url}{source}")
261
+ expires_at = me.get("token_expires_at")
262
+ if expires_at is None:
263
+ typer.echo("token does not expire")
264
+ else:
265
+ expires = datetime.fromisoformat(expires_at).astimezone(UTC)
266
+ typer.echo(f"token expires {expires:%Y-%m-%d %H:%M} UTC")
267
+ warning = connection.token_expiry_warning(active, expires_at, datetime.now(UTC))
268
+ if warning is not None:
269
+ typer.secho(warning, fg="yellow", err=True)
270
+
271
+
272
+ @app.command()
273
+ def update(
274
+ server: str = typer.Option(
275
+ None, "--server", help="Server base URL; defaults to the one you logged in to."
276
+ ),
277
+ skills: bool = typer.Option(
278
+ True,
279
+ "--skills/--no-skills",
280
+ help="After reinstalling, refresh the skills in every directory "
281
+ "`evalkit skills install` wrote to.",
282
+ ),
283
+ ):
284
+ """Reinstall the CLI from PyPI at the server's version.
285
+
286
+ Run it from the project directory: it also moves an `evalkit-py==…` pin in
287
+ ./pyproject.toml to the server's version and runs `uv sync`, so the CLI and the SDK
288
+ match the server again after a deploy. After a reinstall it runs the new
289
+ `evalkit skills install` again in every skills directory installed before, and in
290
+ ./.claude/skills or ./.agents/skills if an earlier install put skills there.
291
+
292
+ Example: evalkit update · evalkit update --no-skills
293
+ """
294
+ server_url = server or _active_connection().url
295
+ run_update(ApiClient(server_url, check_version=False), skills=skills)
296
+
297
+
298
+ @app.command()
299
+ def lint(
300
+ path: Path = typer.Argument(..., help="Case YAML file or directory."),
301
+ offline: bool = typer.Option(
302
+ False,
303
+ "--offline",
304
+ help="Lint locally without a server: the no-server subset of the server lint. "
305
+ "Skips the checks that need server state (registered custom evaluators, "
306
+ "overlay targets).",
307
+ ),
308
+ dataset: str | None = typer.Option(
309
+ None,
310
+ "--dataset",
311
+ help="Dataset these cases sync into. With it, the server also checks overlay "
312
+ "targets against the other datasets in its workspace.",
313
+ ),
314
+ agent: str | None = typer.Option(
315
+ None,
316
+ "--agent",
317
+ help="Agent to check case inputs against (its input_schema). Needs the server; "
318
+ "on demand, not for CI.",
319
+ ),
320
+ ):
321
+ """Lint case YAML without importing it (server-side by default).
322
+
323
+ The server lint also checks fixture keys against the nearest fixtures.yaml,
324
+ so it covers everything --offline does.
325
+
326
+ Example: evalkit lint ./eval-cases --dataset client-cases · evalkit lint --offline datasets
327
+ · evalkit lint ./eval-cases --agent support-triage
328
+ """
329
+ if offline and agent is not None:
330
+ raise ApiError("--agent needs the server; drop --offline")
331
+ if offline:
332
+ all_cases, all_overlays = [], []
333
+ for _, file_path in _yaml_files(path):
334
+ cases, overlays = load_cases_from_yaml(file_path)
335
+ all_cases.extend(cases)
336
+ all_overlays.extend(overlays)
337
+ known = {name for case in all_cases for name in case.evaluators.custom}
338
+ known.update(name for ov in all_overlays for name in ov.evaluators.custom)
339
+ findings = lint_cases(all_cases, all_overlays, known_custom_evaluators=known)
340
+ registry = _nearest_registry(path)
341
+ entries, registry_findings = load_registries([registry] if registry else [])
342
+ findings.extend(registry_findings)
343
+ registry_in_scope = registry is not None and registry.parent == path
344
+ findings.extend(fixture_findings(all_cases, entries, warn_unused=registry_in_scope))
345
+ _print_findings([f.__dict__ for f in findings])
346
+ errors = sum(1 for f in findings if f.severity == "error")
347
+ warnings = sum(1 for f in findings if f.severity == "warning")
348
+ typer.echo(f"{len(all_cases)} case(s): {errors} error(s), {warnings} warning(s)")
349
+ if errors:
350
+ raise typer.Exit(code=1)
351
+ return
352
+ conn = _connection()
353
+ me = conn.username or ""
354
+ params = {"dataset": slug(_parse(dataset, me))} if dataset else {}
355
+ form: dict[str, str] = {}
356
+ if agent:
357
+ ref = _parse(agent, me)
358
+ definition = _agent_definition(ref)
359
+ _require_own_agent(definition, me, "lint against")
360
+ if server_may_drive(definition):
361
+ params["agent"] = slug(ref)
362
+ else:
363
+ local = _with_secrets(_local_config(), scoring=False)
364
+ try:
365
+ schema = asyncio.run(local_input_schema(conn, local, ref))
366
+ except (httpx.HTTPError, ValueError, ApiRequestError) as exc:
367
+ raise ApiError(f"harness {definition.ref}: {exc}") from exc
368
+ form["input_schema"] = json.dumps(schema)
369
+ files = _yaml_files(path)
370
+ registry = _nearest_registry(path)
371
+ if registry is not None:
372
+ files.append((REGISTRY_FILENAME, registry))
373
+ params["warn_unused_fixtures"] = str(registry.parent == path).lower()
374
+ body = _client().post_files("/api/lint", files, params=params, data=form).json()
375
+ _print_findings(body["findings"])
376
+ typer.echo(f"{body['errors']} error(s), {body['warnings']} warning(s)")
377
+ if body["errors"]:
378
+ raise typer.Exit(code=1)
379
+
380
+
381
+ def _sync_and_report(
382
+ dataset_dir: Path,
383
+ *,
384
+ name: str,
385
+ workspace: str,
386
+ revision: GitRevision,
387
+ subpath: str | None = None,
388
+ fixtures_dir: Path | None = None,
389
+ actor: str | None = None,
390
+ ) -> None:
391
+ try:
392
+ report = sync_dataset(
393
+ _client(),
394
+ dataset_dir,
395
+ name=name,
396
+ workspace=workspace,
397
+ revision=revision,
398
+ subpath=subpath,
399
+ fixtures_dir=fixtures_dir,
400
+ actor=actor,
401
+ )
402
+ except ValueError as exc:
403
+ raise ApiError(str(exc)) from exc
404
+ _print_findings(report["findings"])
405
+ if report["aborted"]:
406
+ typer.secho("sync aborted on lint errors", fg="red")
407
+ raise typer.Exit(code=1)
408
+ typer.echo(
409
+ f"synced {slug(Reference(name, workspace))}: created={report['created']} "
410
+ f"patched={report['patched']} deleted={report['deleted']} "
411
+ f"overlays_applied={report['overlays_applied']}"
412
+ )
413
+ review = report.get("review") or {}
414
+ if review.get("now_unreviewed"):
415
+ typer.echo(
416
+ f"{len(review['now_unreviewed'])} approved case(s) changed and are now unreviewed: "
417
+ + ", ".join(review["now_unreviewed"])
418
+ )
419
+ if review.get("carried"):
420
+ typer.echo(
421
+ f"{len(review['carried'])} case(s) updated and still under review: "
422
+ + ", ".join(f"{c['case_id']} ({c['reviewer']})" for c in review["carried"])
423
+ )
424
+
425
+
426
+ @app.command()
427
+ def sync(
428
+ path: str = typer.Argument(
429
+ ..., help="Dataset name under the cases root, or a directory or case file on disk."
430
+ ),
431
+ workspace: str = typer.Option(
432
+ None,
433
+ "--workspace",
434
+ help="Target workspace; defaults to yours. CI passes the shared one, e.g. dev.",
435
+ ),
436
+ version: str | None = typer.Option(
437
+ None, "--version", help="Git sha label; detected from git when omitted."
438
+ ),
439
+ branch: str | None = typer.Option(None, "--branch", help="Detected from git when omitted."),
440
+ dirty: bool | None = typer.Option(
441
+ None, "--dirty/--no-dirty", help="Detected from git when omitted."
442
+ ),
443
+ actor: str | None = typer.Option(
444
+ None, "--actor", help="Record this user as the author of the sync (admin only)."
445
+ ),
446
+ ):
447
+ """Import a dataset into a workspace. The dataset is the directory under the cases
448
+ root (or the directory itself without an evalkit.yaml); a whole dataset prunes cases no
449
+ longer on disk, a subfolder or file patches just its cases. Uploads case YAML plus the
450
+ nearest fixtures.yaml.
451
+
452
+ Example: evalkit sync finance · evalkit sync finance/statement-analysis ·
453
+ evalkit sync datasets/finance --workspace dev --version $GITHUB_SHA
454
+ """
455
+ conn = _connection()
456
+ me = conn.username or ""
457
+ target = workspace or me
458
+ local = _find_local()
459
+ resolved = _resolve(path, local.cases if local else None, me)
460
+ if resolved.dataset_dir is None:
461
+ raise ApiError(f"{path} is not on disk; sync needs a directory or file")
462
+ detected = git_revision(resolved.dataset_dir)
463
+ revision = GitRevision(
464
+ sha=version if version is not None else detected.sha,
465
+ branch=branch if branch is not None else detected.branch,
466
+ dirty=dirty if dirty is not None else detected.dirty,
467
+ )
468
+ _sync_and_report(
469
+ resolved.dataset_dir,
470
+ name=resolved.ref.name,
471
+ workspace=target,
472
+ revision=revision,
473
+ subpath=resolved.subpath,
474
+ fixtures_dir=local.fixtures if local else None,
475
+ actor=actor,
476
+ )
477
+
478
+
479
+ @datasets_app.command("create")
480
+ def datasets_create(
481
+ dataset: str = typer.Argument(..., help="finance (yours) or finance@prod."),
482
+ ):
483
+ """Create an empty suite dataset (writers of the workspace).
484
+
485
+ A promotion target is fed by approvals in its source workspace, so its datasets
486
+ start here. Example: evalkit datasets create finance@prod
487
+ """
488
+ ref = _parse(dataset, _connection().username or "")
489
+ _client().post("/api/datasets", json={"name": ref.name, "workspace": ref.workspace})
490
+ typer.echo(f"created {slug(ref)}")
491
+
492
+
493
+ @datasets_app.command("list")
494
+ def datasets_list(
495
+ dataset: str | None = typer.Argument(
496
+ None, help="List this dataset's cases instead: finance (yours) or finance@dev."
497
+ ),
498
+ state: CaseState | None = typer.Option(
499
+ None, "--state", help="Only cases in this review state (shared workspaces only)."
500
+ ),
501
+ tag: str | None = typer.Option(None, "--tag", help="Only cases carrying this tag."),
502
+ ):
503
+ """List datasets with kind and version, or one dataset's cases.
504
+
505
+ With a dataset, prints each case's id, review state (shared workspaces only; `-`
506
+ elsewhere), tags and fixture keys.
507
+
508
+ Example: evalkit datasets list · evalkit datasets list finance@dev --state approved
509
+ · evalkit datasets list finance --tag statement-analysis
510
+ """
511
+ if dataset is None:
512
+ if state is not None or tag is not None:
513
+ raise ApiError("--state and --tag filter a dataset's cases; pass a dataset")
514
+ for ds in _client().get("/api/datasets").json():
515
+ last = ds.get("last_sync") or {}
516
+ sha = (last.get("detail") or {}).get("sha") or "-"
517
+ typer.echo(f"{ds['ref']:<32} kind={ds['kind']:<10} sha={sha[:12]}")
518
+ return
519
+ dataset_slug = slug(_parse(dataset, _connection().username or ""))
520
+ params = {"tag": tag} if tag is not None else {}
521
+ rows = _client().get(f"/api/datasets/{encode(dataset_slug)}/cases", params=params).json()
522
+ if state is not None and rows and all(row["review"] is None for row in rows):
523
+ raise ApiError(f"{dataset_slug} has no review state; only suites in shared workspaces do")
524
+ shown = [row for row in rows if state is None or (row["review"] or {}).get("state") == state]
525
+ for row in shown:
526
+ review = row["review"] or {}
527
+ fixtures = [f["key"] for f in row["files"]]
528
+ typer.echo(
529
+ f"{row['case_id']:<40} {review.get('state', '-'):<18} "
530
+ f"tags={','.join(row['split_tags']) or '-'} "
531
+ f"fixtures={','.join(fixtures) or '-'}"
532
+ )
533
+ typer.echo(f"{len(shown)} case(s) in {dataset_slug}")
534
+
535
+
536
+ class _Progress:
537
+ def __init__(self) -> None:
538
+ self.exp_id: int | None = None
539
+
540
+ def __call__(self, line: str) -> None:
541
+ if line.startswith("experiment "):
542
+ self.exp_id = int(line.split()[1])
543
+ typer.echo(line)
544
+
545
+
546
+ def _warn_dropped(dropped: list[str]) -> None:
547
+ if dropped:
548
+ typer.secho(dropped_cases_text(dropped), fg="yellow")
549
+
550
+
551
+ def _run_on_server(
552
+ dataset_slug: str,
553
+ agent_slug: str,
554
+ selection: Selection,
555
+ repetitions: int,
556
+ model: str | None,
557
+ resume: int | None,
558
+ revision: GitRevision,
559
+ judge: dict | None,
560
+ ) -> int:
561
+ client = _client()
562
+ if resume:
563
+ exp_id = int(client.post(f"/api/experiments/{resume}/resume").json()["id"])
564
+ else:
565
+ payload = {
566
+ "dataset": dataset_slug,
567
+ "agent": agent_slug,
568
+ "repetitions": repetitions,
569
+ "selection": selection.model_dump(exclude_none=True),
570
+ "model": model,
571
+ "cases": revision.model_dump(exclude_none=True) or None,
572
+ "evalkit_version": version.current(),
573
+ "judge": judge,
574
+ }
575
+ created = client.post("/api/experiments", json=payload).json()
576
+ exp_id = int(created["id"])
577
+ _warn_dropped(created.get("dropped_cases") or [])
578
+ typer.echo(f"experiment {exp_id}: {_experiment_url(exp_id)}")
579
+ last = None
580
+ while True:
581
+ body = client.get(f"/api/experiments/{exp_id}").json()
582
+ if body["status"] != last:
583
+ typer.echo(f" {body['status']} ({_progress_text(body['progress'])})")
584
+ last = body["status"]
585
+ if body["status"] in {"completed", "failed"}:
586
+ break
587
+ time.sleep(RUN_POLL_SECONDS)
588
+ if body["status"] == "failed":
589
+ typer.secho(f"experiment failed: {body.get('status_error')}", fg="red")
590
+ raise typer.Exit(code=1)
591
+ return exp_id
592
+
593
+
594
+ @app.command()
595
+ def run(
596
+ dataset: str = typer.Argument(
597
+ ...,
598
+ help="finance · finance@dev · finance/<split> · finance/<file>.yaml · a path",
599
+ ),
600
+ agent: str = typer.Option(
601
+ ...,
602
+ "--agent",
603
+ help="myagent (yours, your laptop drives it) or ouragent@dev (the server drives it).",
604
+ ),
605
+ runs: int = typer.Option(1, "--runs", help="Repetitions per case."),
606
+ case_id: list[str] = typer.Option(None, "--case-id", help="Run only these case ids."),
607
+ model: str = typer.Option(None, "--model", help="Model override (if the harness accepts it)."),
608
+ resume: int = typer.Option(None, "--resume", help="Resume an experiment by id."),
609
+ judge_model: str | None = typer.Option(
610
+ None, "--judge-model", help="Judge model for this experiment; default: the server's."
611
+ ),
612
+ judge_base_url: str | None = typer.Option(
613
+ None, "--judge-base-url", help="Judge endpoint; its key is credentials.judges[url]."
614
+ ),
615
+ judge_effort: str | None = typer.Option(None, "--judge-effort", help="Judge reasoning effort."),
616
+ ):
617
+ """Run an experiment. Your own agent runs from this machine; results land on the server.
618
+
619
+ Runs every selected case, whatever its review state. Checks every selected
620
+ case's input against the agent's input_schema (or, when it declares none,
621
+ the default: a required string `question`) and refuses the whole experiment
622
+ if any case fails, listing each one. `evalkit lint <dir> --agent <name>`
623
+ shows the same errors before you sync.
624
+
625
+ Example: evalkit run finance --agent research-agent ·
626
+ evalkit run finance@dev/statement-analysis --agent ouragent@dev
627
+ """
628
+ conn = _connection()
629
+ me = conn.username or ""
630
+ agent_ref = _parse(agent, me)
631
+ definition = _agent_definition(agent_ref)
632
+ route = driver(definition)
633
+ if route is Driver.SDK:
634
+ raise ApiError(
635
+ f"{definition.ref} is a code-driven agent (no harness_url); "
636
+ "run it with evalkit.sdk.run_experiment, or if it serves the Harness API, "
637
+ f"set its URL: evalkit agents edit {definition.ref} --harness-url URL"
638
+ )
639
+ if route is Driver.CLI and definition.workspace != me:
640
+ raise ApiError(
641
+ f"{definition.ref} belongs to {definition.workspace}'s workspace; "
642
+ "only its owner can run it"
643
+ )
644
+ local = _find_local()
645
+ if local is not None and route is Driver.CLI:
646
+ local = _with_secrets(local, scoring=True)
647
+ _require_agent_key(local, conn, definition)
648
+ resolved = _resolve(dataset, local.cases if local else None, me)
649
+ selection = Selection(case_ids=list(case_id)) if case_id else resolved.selection
650
+ revision = git_revision(resolved.dataset_dir) if resolved.dataset_dir else GitRevision()
651
+ if resolved.dataset_dir is not None and resume is None:
652
+ _sync_and_report(
653
+ resolved.dataset_dir,
654
+ name=resolved.ref.name,
655
+ workspace=me,
656
+ revision=revision,
657
+ fixtures_dir=local.fixtures if local else None,
658
+ )
659
+ dataset_slug = slug(resolved.ref)
660
+ judge = _judge_override(judge_model, judge_base_url, judge_effort)
661
+ if route is Driver.SERVER:
662
+ exp_id = _run_on_server(
663
+ dataset_slug,
664
+ slug(agent_ref),
665
+ selection,
666
+ runs,
667
+ model,
668
+ resume,
669
+ revision,
670
+ judge,
671
+ )
672
+ else:
673
+ if local is None:
674
+ raise ApiError("no evalkit.yaml found above the current directory")
675
+ progress = _Progress()
676
+ try:
677
+ exp_id = asyncio.run(
678
+ run_local(
679
+ conn,
680
+ local,
681
+ agent=agent_ref,
682
+ dataset_slug=dataset_slug,
683
+ selection=selection,
684
+ repetitions=runs,
685
+ model=model,
686
+ resume=resume,
687
+ revision=revision,
688
+ progress=progress,
689
+ judge=JudgeSettings.model_validate(judge) if judge else None,
690
+ )
691
+ )
692
+ except KeyboardInterrupt:
693
+ if progress.exp_id is not None:
694
+ asyncio.run(mark_interrupted(conn, progress.exp_id, me))
695
+ typer.secho("interrupted", fg="red")
696
+ raise typer.Exit(code=130) from None
697
+ except ScoringFailed as exc:
698
+ raise ApiError(f"{exc}\nfinish it with: evalkit evaluate {exc.exp_id}") from exc
699
+ except (VersionMismatch, ApiRequestError, ValueError, KeyError) as exc:
700
+ raise ApiError(str(exc)) from exc
701
+ _render_summary(_client().get(f"/api/experiments/{exp_id}/scorecard").json())
702
+
703
+
704
+ def _judge_override(model: str | None, base_url: str | None, effort: str | None) -> dict | None:
705
+ changes = {
706
+ key: value
707
+ for key, value in {
708
+ "model": model,
709
+ "base_url": base_url,
710
+ "reasoning_effort": effort,
711
+ }.items()
712
+ if value is not None
713
+ }
714
+ if not changes:
715
+ return None
716
+ return {**_client().get("/api/judge-default").json(), **changes}
717
+
718
+
719
+ def _require_agent_key(
720
+ local: LocalConfig, conn: connection.Connection, agent: AgentDefinition
721
+ ) -> None:
722
+ try:
723
+ local_credentials(local, conn).key_for("agent", agent.ref)
724
+ except MissingCredential as exc:
725
+ raise ApiError(str(exc)) from exc
726
+
727
+
728
+ def _agent_definition(ref: Reference) -> AgentDefinition:
729
+ return definition_of(_client().get(f"/api/agents/{encode(explicit(ref))}").json())
730
+
731
+
732
+ @app.command()
733
+ def evaluate(
734
+ experiment_id: int,
735
+ refetch_trajectories: bool = typer.Option(
736
+ False,
737
+ "--refetch-trajectories",
738
+ help="Poll the collector again for runs whose trajectory was partial or unavailable, "
739
+ "then re-score them.",
740
+ ),
741
+ ):
742
+ """Re-score an experiment's recorded runs from this machine (idempotent).
743
+
744
+ Scores already recorded are kept; evaluators that errored run again.
745
+
746
+ Refuses experiments that used Python evaluators; re-score those with
747
+ evalkit.sdk.evaluate_experiment from the script that defines them.
748
+
749
+ Example: evalkit evaluate <experiment-id> --refetch-trajectories
750
+ """
751
+ local = _with_secrets(_local_config(), scoring=True)
752
+ conn = _connection()
753
+ body = _client().get(f"/api/experiments/{experiment_id}").json()
754
+ python_evaluators = body["config"]["snapshot"].get("code_evaluators") or []
755
+ if python_evaluators:
756
+ raise ApiError(
757
+ f"experiment {experiment_id} used python evaluators "
758
+ f"{', '.join(python_evaluators)}; re-score it with evalkit.sdk.evaluate_experiment "
759
+ "from the script that defines them"
760
+ )
761
+ try:
762
+ asyncio.run(
763
+ evaluate_local(
764
+ conn,
765
+ local,
766
+ experiment_id,
767
+ progress=typer.echo,
768
+ refetch_trajectories=refetch_trajectories,
769
+ )
770
+ )
771
+ except ScoringFailed as exc:
772
+ raise ApiError(f"{exc}\nfinish it with: evalkit evaluate {experiment_id}") from exc
773
+ except (VersionMismatch, ApiRequestError, ValueError, KeyError) as exc:
774
+ raise ApiError(str(exc)) from exc
775
+ typer.echo(f"evaluated {experiment_id}")
776
+
777
+
778
+ def _progress_text(progress: dict) -> str:
779
+ total = progress["total"]
780
+ return f"{progress['ran']}/{total} ran, {progress['evaluated']}/{total} evaluated"
781
+
782
+
783
+ def _experiment_url(exp_id: int) -> str:
784
+ return f"{_connection().url.rstrip('/')}/experiments/{exp_id}"
785
+
786
+
787
+ def _lease_text(body: dict) -> str:
788
+ if body["status"] not in {"running", "evaluating"} or not body.get("lease_holder"):
789
+ return ""
790
+ expires = datetime.fromisoformat(body["lease_expires_at"])
791
+ left = int((expires - datetime.now(UTC)).total_seconds())
792
+ if left < 0:
793
+ return f"\n held by {body['lease_holder']} (lease expired {-left}s ago; runner stopped?)"
794
+ return f"\n held by {body['lease_holder']} (lease expires in {left}s)"
795
+
796
+
797
+ @app.command()
798
+ def status(experiment_id: int):
799
+ """Show an experiment's status, progress and, while it runs, who holds it.
800
+
801
+ Example: evalkit status 42
802
+ """
803
+ body = _client().get(f"/api/experiments/{experiment_id}").json()
804
+ typer.echo(
805
+ f"experiment {body['id']} #{body['number']} of {body['dataset']} agent={body['agent']}\n"
806
+ f" status={body['status']} {_progress_text(body['progress'])}{_lease_text(body)}"
807
+ )
808
+ if body.get("status_error"):
809
+ typer.secho(f" error: {body['status_error']}", fg="red")
810
+
811
+
812
+ def _render_summary(report: dict) -> None:
813
+ for line in summary_lines(report):
814
+ typer.echo(line)
815
+
816
+
817
+ @app.command()
818
+ def summary(
819
+ experiment_id: int,
820
+ baseline: int = typer.Option(None, "--baseline", help="Baseline experiment id for deltas."),
821
+ ):
822
+ """Render an experiment's scorecard as a console table.
823
+
824
+ Example: evalkit summary <experiment-id> --baseline <older-id>
825
+ """
826
+ params = {"baseline_id": baseline} if baseline else {}
827
+ _render_summary(
828
+ _client().get(f"/api/experiments/{experiment_id}/scorecard", params=params).json()
829
+ )
830
+
831
+
832
+ def _require_own_agent(definition: AgentDefinition, me: str, action: str) -> None:
833
+ if not definition.workspace_is_shared and definition.workspace != me:
834
+ raise ApiError(
835
+ f"{definition.ref} belongs to {definition.workspace}'s workspace; "
836
+ f"only its owner can {action} it"
837
+ )
838
+
839
+
840
+ @harness_app.command("check")
841
+ def harness_check(
842
+ agent: str = typer.Argument(
843
+ ..., help="myagent (yours, your laptop drives it) or ouragent@dev (the server drives it)."
844
+ ),
845
+ ):
846
+ """Exercise a harness's Harness API and print a per-capability table.
847
+
848
+ Example: evalkit harness check myagent · evalkit harness check ouragent@dev
849
+ """
850
+ conn = _connection()
851
+ ref = _parse(agent, conn.username or "")
852
+ definition = _agent_definition(ref)
853
+ _require_own_agent(definition, conn.username or "", "check")
854
+ if definition.archived:
855
+ raise ApiError(f"agent {definition.ref} is archived")
856
+ if driver(definition) is Driver.SDK:
857
+ raise ApiError(
858
+ f"{definition.ref} is a code-driven agent (no harness_url); "
859
+ "use evalkit traces check AGENT SESSION_ID"
860
+ )
861
+ if server_may_drive(definition):
862
+ checks = _client().post(f"/api/agents/{encode(slug(ref))}/check").json()["checks"]
863
+ else:
864
+ local = _with_secrets(_local_config(), scoring=False)
865
+ try:
866
+ checks = asyncio.run(check_local(conn, local, ref))
867
+ except (ValueError, KeyError, ApiRequestError) as exc:
868
+ raise ApiError(str(exc)) from exc
869
+ failed = False
870
+ for capability, entry in checks.items():
871
+ marker = {"pass": "✓", "warn": "!", "fail": "✗", "skipped": "-"}[entry["status"]]
872
+ typer.echo(f" {marker} {capability:<14} {entry['detail']}")
873
+ failed = failed or entry["status"] == "fail"
874
+ if failed:
875
+ raise typer.Exit(code=1)
876
+
877
+
878
+ @users_app.command("add")
879
+ def users_add(username: str, password: str = typer.Option(..., prompt=True, hide_input=True)):
880
+ """Create a user (admin only). Example: evalkit users add alice"""
881
+ _client().post("/api/users", json={"username": username, "password": password})
882
+ typer.echo(f"created {username}")
883
+
884
+
885
+ @users_app.command("archive")
886
+ def users_archive(username: str):
887
+ """Archive a user: revoke their login tokens, block login, archive their workspace.
888
+
889
+ Agent tokens they minted keep working: they belong to the agent.
890
+
891
+ Users are never deleted, so their experiments keep an owner and the name
892
+ stays reserved. Admin only. Example: evalkit users archive alice
893
+ """
894
+ _client().post(f"/api/users/{username}/archive")
895
+ typer.echo(f"archived {username}")
896
+
897
+
898
+ @users_app.command("restore")
899
+ def users_restore(username: str):
900
+ """Let an archived user log in again (admin only). Example: evalkit users restore alice
901
+
902
+ Their personal workspace is restored too when it was archived with them.
903
+ """
904
+ _client().post(f"/api/users/{username}/restore")
905
+ typer.echo(f"restored {username}")
906
+
907
+
908
+ @users_app.command("set-password")
909
+ def users_set_password(
910
+ username: str,
911
+ password: str = typer.Option(..., prompt=True, hide_input=True, confirmation_prompt=True),
912
+ ):
913
+ """Set a user's password and sign them out everywhere (admin only).
914
+
915
+ Example: evalkit users set-password alice
916
+ """
917
+ _client().post(f"/api/users/{username}/password", json={"password": password})
918
+ typer.echo(f"password set for {username}")
919
+
920
+
921
+ @users_app.command("promote")
922
+ def users_promote(username: str):
923
+ """Make a user an admin (admin only). Example: evalkit users promote alice"""
924
+ _client().post(f"/api/users/{username}/promote")
925
+ typer.echo(f"{username} is an admin")
926
+
927
+
928
+ @users_app.command("demote")
929
+ def users_demote(username: str):
930
+ """Remove a user's admin flag; the last admin stays. Example: evalkit users demote alice"""
931
+ _client().post(f"/api/users/{username}/demote")
932
+ typer.echo(f"{username} is no longer an admin")
933
+
934
+
935
+ @users_app.command("list")
936
+ def users_list():
937
+ """List users (admin only). Example: evalkit users list"""
938
+ for user in _client().get("/api/users").json():
939
+ flags = " ".join(
940
+ flag for flag, on in (("ADMIN", user["is_admin"]), ("ARCHIVED", user["archived"])) if on
941
+ )
942
+ typer.echo(f"{user['username']:<24} {flags:<16} created {user['created_at']}")
943
+
944
+
945
+ DOCS_DIR = "evalkit-docs"
946
+ INSTALL_LEAVES_OUT = shutil.ignore_patterns(".env", "*.whl", "__pycache__", "*.pyc")
947
+
948
+
949
+ @skills_app.command("install")
950
+ def skills_install(
951
+ target: Path = typer.Option(
952
+ None,
953
+ "--target",
954
+ help="Skills dir; default .claude/skills/ if .claude/ exists, else .agents/skills/",
955
+ ),
956
+ ):
957
+ """Copy the bundled skills, and the user docs they link to, into this repo's agent-skills
958
+ directory.
959
+
960
+ The docs go to evalkit-docs/ beside the skills. A directory an earlier install put there
961
+ that this version no longer ships is removed; other directories are left alone.
962
+ `evalkit update` refreshes every directory installed this way.
963
+
964
+ Example: evalkit skills install
965
+ """
966
+ root = skills_root()
967
+ skills = sorted(p for p in root.iterdir() if (p / "SKILL.md").is_file())
968
+ if not skills:
969
+ raise ApiError(f"no skills found under {root}")
970
+ if target is None:
971
+ target = Path(".claude/skills") if Path(".claude").is_dir() else Path(".agents/skills")
972
+ target.mkdir(parents=True, exist_ok=True)
973
+ sources = {skill.name: skill for skill in skills} | {DOCS_DIR: docs_root()}
974
+ manifest = target / INSTALL_MANIFEST
975
+ previous = manifest.read_text().split() if manifest.is_file() else []
976
+ for name in previous:
977
+ if name not in sources and (target / name).is_dir():
978
+ shutil.rmtree(target / name)
979
+ typer.echo(f"removed {name} (no longer shipped)")
980
+ for name, source in sources.items():
981
+ destination = target / name
982
+ if destination.exists():
983
+ shutil.rmtree(destination)
984
+ shutil.copytree(source, destination, ignore=INSTALL_LEAVES_OUT)
985
+ typer.echo(f"installed {name} → {destination}")
986
+ manifest.write_text("".join(f"{name}\n" for name in sources))
987
+ record_skill_target(target)
988
+
989
+
990
+ @contracts_app.command("export")
991
+ def contracts_export(
992
+ target: Path = typer.Option(
993
+ Path("contracts"), "--target", help="Directory to write the contract files into."
994
+ ),
995
+ ):
996
+ """Write the bundled API contracts (OpenAPI + accepted ATIF versions) to a directory.
997
+
998
+ Exports harness-api-v1.json, custom-evaluator-api-v1.json and
999
+ atif-accepted.json, matching the installed evalkit version.
1000
+
1001
+ Example: evalkit contracts export --target ./contracts
1002
+ """
1003
+ root = contracts_root()
1004
+ files = sorted(p for p in root.iterdir() if p.suffix == ".json")
1005
+ if not files:
1006
+ raise ApiError(f"no contract files found under {root}")
1007
+ target.mkdir(parents=True, exist_ok=True)
1008
+ for file in files:
1009
+ shutil.copyfile(file, target / file.name)
1010
+ typer.echo(f"exported {file.name} → {target / file.name}")
1011
+
1012
+
1013
+ @app.command("init-server")
1014
+ def init_server_command(
1015
+ directory: Path = typer.Option(
1016
+ init_server.DEFAULT_DIR, "--dir", help="Directory for the try kit, config.yaml and .env."
1017
+ ),
1018
+ database_url: str | None = typer.Option(
1019
+ None,
1020
+ "--database-url",
1021
+ envvar="EVALKIT_DATABASE_URL",
1022
+ help="Postgres URL; the database is created when missing.",
1023
+ ),
1024
+ llm_base_url: str | None = typer.Option(
1025
+ None,
1026
+ "--llm-base-url",
1027
+ envvar="LLM_BASE_URL",
1028
+ help="OpenAI-compatible endpoint for the judge and the sample agent.",
1029
+ ),
1030
+ llm_model: str | None = typer.Option(
1031
+ None, "--llm-model", envvar="LLM_MODEL", help="Model name at that endpoint."
1032
+ ),
1033
+ llm_api_key: str | None = typer.Option(
1034
+ None, "--llm-api-key", envvar="LLM_API_KEY", help="API key for that endpoint."
1035
+ ),
1036
+ admin_password: str | None = typer.Option(
1037
+ None,
1038
+ "--admin-password",
1039
+ envvar="EVALKIT_ADMIN_PASSWORD",
1040
+ help="Password of the `admin` user you log in as.",
1041
+ ),
1042
+ yes: bool = typer.Option(False, "--yes", "-y", help="Never prompt; use flags, env, defaults."),
1043
+ force: bool = typer.Option(
1044
+ False,
1045
+ "--force",
1046
+ help="Overwrite an existing kit in --dir: config.yaml, .env and the kit's files, "
1047
+ "including your edits to its cases and scripts.",
1048
+ ),
1049
+ kit_only: bool = typer.Option(
1050
+ False, "--kit-only", help="Only copy the try kit; no database, config.yaml or .env."
1051
+ ),
1052
+ ):
1053
+ """Set up an evalkit server on this machine, against a Postgres you already run.
1054
+
1055
+ Copies the try kit (sample agent, cases, run.py) into --dir.
1056
+
1057
+ Creates the database when missing and migrates it.
1058
+
1059
+ Writes config.yaml (blobs under <dir>/data) and .env (your LLM, the admin password).
1060
+
1061
+ Each value comes from its flag or env var; anything missing is prompted for.
1062
+
1063
+ Example: evalkit init-server · evalkit init-server --kit-only --dir ./evalkit-try
1064
+ """
1065
+ existing = init_server.kit_files_in(directory)
1066
+ if existing and not force:
1067
+ raise ApiError(
1068
+ f"{directory} already holds a try kit ({', '.join(existing)}); "
1069
+ "pass --force to overwrite it, edits included"
1070
+ )
1071
+ if kit_only:
1072
+ init_server.copy_kit(directory, docker=True)
1073
+ typer.echo(f"copied the try kit to {directory}")
1074
+ return
1075
+ if server_commands is None:
1076
+ raise ApiError(
1077
+ "`evalkit init-server` needs the server extra — pip install 'evalkit-py[server]'"
1078
+ )
1079
+ flags = {
1080
+ "database_url": database_url,
1081
+ "llm_base_url": llm_base_url,
1082
+ "llm_model": llm_model,
1083
+ "llm_api_key": llm_api_key,
1084
+ "admin_password": admin_password,
1085
+ }
1086
+ answers = init_server.resolve(flags, yes=yes)
1087
+ typer.echo(init_server.DATABASE_NOTICE)
1088
+ created = server_commands.init_database(answers.database_url)
1089
+ typer.echo(
1090
+ "created the database and migrated it"
1091
+ if created
1092
+ else "the database already existed; migrated it"
1093
+ )
1094
+ saved_key = init_server.existing_encryption_key(directory)
1095
+ new_key = saved_key or server_commands.new_encryption_key()
1096
+ encryption_key: str | None = new_key
1097
+ if created or not server_commands.has_llm_setup(answers.database_url):
1098
+ server_commands.register_default_judge(
1099
+ answers.database_url,
1100
+ new_key,
1101
+ answers.llm_base_url,
1102
+ answers.llm_api_key,
1103
+ answers.llm_model,
1104
+ )
1105
+ typer.echo(f"registered {answers.llm_base_url} as the default judge ({answers.llm_model})")
1106
+ else:
1107
+ typer.secho(
1108
+ "the database already has LLM providers or a default judge; left them as they are. "
1109
+ "Change them with `evalkit llm-providers` and `evalkit judge-default set`. A server "
1110
+ "using this database needs the encryption_key it was set up with.",
1111
+ fg="yellow",
1112
+ )
1113
+ if saved_key is None:
1114
+ encryption_key = None
1115
+ typer.secho(
1116
+ f"set EVALKIT_ENCRYPTION_KEY in {directory / '.env'} to that key before "
1117
+ "`evalkit serve`; the line is there, commented out.",
1118
+ fg="yellow",
1119
+ )
1120
+ init_server.copy_kit(directory, docker=False)
1121
+ init_server.write_server_files(directory, answers, encryption_key)
1122
+ typer.echo(f"set up {directory}: config.yaml, .env and the try kit")
1123
+ typer.echo("next, start the server:")
1124
+ typer.echo(f" cd {directory} && evalkit serve")
1125
+ typer.echo("then, in another terminal from the same folder, log in:")
1126
+ typer.echo(" evalkit login --url http://localhost:8020 --username admin")
1127
+ typer.echo(" (this replaces a login you saved; Try it locally step 1 shows how to keep it)")
1128
+ typer.echo("and follow Try it locally from step 2 (the CLI is already installed)")
1129
+
1130
+
1131
+ @app.command()
1132
+ def serve(
1133
+ config: Path = typer.Option(Path("config.yaml"), "--config", help="Path to config.yaml."),
1134
+ host: str = typer.Option("127.0.0.1", "--host"),
1135
+ port: int = typer.Option(8020, "--port"),
1136
+ ):
1137
+ """Start the evalkit server (API + UI).
1138
+
1139
+ Example: evalkit serve --config /etc/evalkit/config.yaml
1140
+ """
1141
+ if server_commands is None:
1142
+ raise ApiError("`evalkit serve` needs the server extra — pip install 'evalkit-py[server]'")
1143
+ server_commands.serve(_server_config(config), host, port)
1144
+
1145
+
1146
+ @db_app.command("upgrade")
1147
+ def db_upgrade(
1148
+ config: Path = typer.Option(Path("config.yaml"), "--config", help="Path to config.yaml."),
1149
+ revision: str = typer.Option("head", "--revision", help="Stop at this revision, e.g. 0006."),
1150
+ ):
1151
+ """Run schema migrations against the configured database.
1152
+
1153
+ Example: evalkit db upgrade --config /etc/evalkit/config.yaml --revision 0006
1154
+ """
1155
+ if server_commands is None:
1156
+ raise ApiError(
1157
+ "`evalkit db upgrade` needs the server extra — pip install 'evalkit-py[server]'"
1158
+ )
1159
+ server_commands.db_upgrade(_server_config(config), revision)
1160
+ typer.echo(f"migrations applied up to {revision}")
1161
+
1162
+
1163
+ @db_app.command("rekey")
1164
+ def db_rekey(
1165
+ config: Path = typer.Option(Path("config.yaml"), "--config", help="Path to config.yaml."),
1166
+ old_key_env: str = typer.Option(..., "--old-key-env", help="Env var with the old key."),
1167
+ new_key_env: str = typer.Option(..., "--new-key-env", help="Env var with the new key."),
1168
+ ):
1169
+ """Re-encrypt every stored API key (agents, remote evaluators, LLM providers, trace
1170
+ sources) after an encryption_key change (server-side).
1171
+
1172
+ Example: evalkit db rekey --config config.yaml --old-key-env OLD --new-key-env NEW
1173
+ """
1174
+ if server_commands is None:
1175
+ raise ApiError("`db rekey` needs the server extra — pip install 'evalkit-py[server]'")
1176
+ missing = [name for name in (old_key_env, new_key_env) if name not in os.environ]
1177
+ if missing:
1178
+ raise ApiError(f"set {', '.join(missing)} first")
1179
+ count = asyncio.run(
1180
+ server_commands.rekey(
1181
+ _server_config(config), os.environ[old_key_env], os.environ[new_key_env]
1182
+ )
1183
+ )
1184
+ typer.echo(f"re-encrypted {count} stored API key(s)")
1185
+
1186
+
1187
+ if __name__ == "__main__":
1188
+ app()
1189
+
1190
+
1191
+ def _selection(tag: str | None, case_id: list[str] | None) -> dict:
1192
+ if case_id:
1193
+ return {"case_ids": list(case_id)}
1194
+ if tag:
1195
+ return {"tag": tag}
1196
+ return {"all": True}
1197
+
1198
+
1199
+ def _dataset_ref(dataset: str) -> str:
1200
+ return encode(slug(_parse(dataset, _connection().username or "")))
1201
+
1202
+
1203
+ def _case_path(dataset: str, case_id: str) -> str:
1204
+ return f"/api/datasets/{_dataset_ref(dataset)}/cases/{case_id}"
1205
+
1206
+
1207
+ def _fmt_comment(comment: dict, *, indent: int) -> str:
1208
+ flag = " [resolved]" if comment["resolved"] else ""
1209
+ pad = " " * indent
1210
+ head = f"{pad}#{comment['id']} {comment['author']} · {comment['created_at']}{flag}"
1211
+ return f"{head}\n{pad}{comment['body']}"
1212
+
1213
+
1214
+ def _post_decision(dataset: str, case_id: str, action: str, comment: str | None) -> None:
1215
+ body = {"action": action, "comment": comment}
1216
+ result = _client().post(f"{_case_path(dataset, case_id)}/review", json=body).json()
1217
+ typer.echo(f"{case_id}: {result['state']}")
1218
+
1219
+
1220
+ @review_app.command("request")
1221
+ def review_request(
1222
+ dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
1223
+ reviewer: str = typer.Option(..., "--reviewer", help="Reviewer username."),
1224
+ tag: str = typer.Option(None, "--tag", help="Only cases carrying this tag."),
1225
+ case_id: list[str] = typer.Option(None, "--case-id", help="Only these case ids."),
1226
+ ):
1227
+ """Ask a reviewer to look at cases (all cases when no selector is given).
1228
+
1229
+ Example: evalkit review request finance --case-id mf-industry-07 --reviewer sme@client
1230
+ """
1231
+ body = {"selection": _selection(tag, case_id), "reviewer": reviewer}
1232
+ requested = (
1233
+ _client().post(f"/api/datasets/{_dataset_ref(dataset)}/review-requests", json=body).json()
1234
+ )
1235
+ typer.echo(f"requested review from {reviewer} on {len(requested['requested'])} case(s)")
1236
+
1237
+
1238
+ @review_app.command("show")
1239
+ def review_show(
1240
+ dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
1241
+ case_id: str = typer.Argument(None, help="Case id; omit to list every case's state."),
1242
+ ):
1243
+ """Show review state for a dataset, or state, changelog and comments for one case.
1244
+
1245
+ Example: evalkit review show finance us10k-conv-04
1246
+ """
1247
+ client = _client()
1248
+ rows = client.get(f"/api/datasets/{_dataset_ref(dataset)}/cases").json()
1249
+ if case_id is None:
1250
+ for ex in rows:
1251
+ review = ex.get("review") or {}
1252
+ typer.echo(
1253
+ f"{ex['case_id']:<40} {review.get('state', '-'):<18} "
1254
+ f"{review.get('requested_to') or ''}"
1255
+ )
1256
+ return
1257
+ match = next((ex for ex in rows if ex["case_id"] == case_id), None)
1258
+ if match is None:
1259
+ raise ApiError(f"unknown case: {case_id}")
1260
+ review = match.get("review") or {}
1261
+ typer.echo(f"state: {review.get('state', '-')}")
1262
+ if review.get("requested_to"):
1263
+ typer.echo(f"requested to: {review['requested_to']}")
1264
+ typer.echo("changelog:")
1265
+ for event in client.get(f"{_case_path(dataset, case_id)}/events").json()["events"]:
1266
+ target = f" → {event['target']}" if event.get("target") else ""
1267
+ typer.echo(
1268
+ f" {event['created_at']} {event['kind']:<18} {event['actor']}{target} "
1269
+ f"[{event['content_hash']}]"
1270
+ )
1271
+ typer.echo("comments:")
1272
+ for thread in client.get(f"{_case_path(dataset, case_id)}/comments").json()["threads"]:
1273
+ typer.echo(_fmt_comment(thread, indent=2))
1274
+ for reply in thread["replies"]:
1275
+ typer.echo(_fmt_comment(reply, indent=6))
1276
+
1277
+
1278
+ @review_app.command("approve")
1279
+ def review_approve(
1280
+ dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
1281
+ case_id: str = typer.Argument(..., help="Case id."),
1282
+ comment: str = typer.Option(None, "--comment", help="Optional note."),
1283
+ ):
1284
+ """Approve a case (reviewers only).
1285
+
1286
+ Example: evalkit review approve finance us10k-conv-04
1287
+ """
1288
+ _post_decision(dataset, case_id, "approve", comment)
1289
+
1290
+
1291
+ @review_app.command("request-changes")
1292
+ def review_request_changes(
1293
+ dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
1294
+ case_id: str = typer.Argument(..., help="Case id."),
1295
+ comment: str = typer.Option(..., "--comment", help="What needs to change."),
1296
+ ):
1297
+ """Send a case back to its author with a comment (reviewers only).
1298
+
1299
+ Example: evalkit review request-changes finance us10k-conv-04 --comment "anchor the date"
1300
+ """
1301
+ _post_decision(dataset, case_id, "request_changes", comment)
1302
+
1303
+
1304
+ @review_app.command("comment")
1305
+ def review_comment(
1306
+ dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
1307
+ case_id: str = typer.Argument(..., help="Case id."),
1308
+ text: str = typer.Argument(..., help="Comment body."),
1309
+ reply_to: int = typer.Option(None, "--reply-to", help="Comment id to reply to."),
1310
+ ):
1311
+ """Comment on a case, or reply to a comment.
1312
+
1313
+ Example: evalkit review comment finance us10k-conv-04 "which fiscal year?"
1314
+ """
1315
+ body = {"body": text, "parent_id": reply_to}
1316
+ stored = _client().post(f"{_case_path(dataset, case_id)}/comments", json=body).json()
1317
+ typer.echo(f"comment {stored['id']} on {case_id}")
1318
+
1319
+
1320
+ @review_app.command("resolve")
1321
+ def review_resolve(
1322
+ dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
1323
+ case_id: str = typer.Argument(..., help="Case id."),
1324
+ comment_id: int = typer.Argument(..., help="Comment id."),
1325
+ undo: bool = typer.Option(False, "--undo", help="Mark the comment unresolved again."),
1326
+ ):
1327
+ """Resolve (or with --undo, unresolve) a comment.
1328
+
1329
+ Example: evalkit review resolve finance us10k-conv-04 12
1330
+ """
1331
+ result = (
1332
+ _client()
1333
+ .patch(f"{_case_path(dataset, case_id)}/comments/{comment_id}", json={"resolved": not undo})
1334
+ .json()
1335
+ )
1336
+ typer.echo(f"comment {result['id']} {'resolved' if result['resolved'] else 'unresolved'}")
1337
+
1338
+
1339
+ @review_app.command("queue")
1340
+ def review_queue(
1341
+ reviewer: str = typer.Option(None, "--reviewer", help="Whose queue; defaults to you."),
1342
+ everyone: bool = typer.Option(False, "--all", help="Every case under review."),
1343
+ changes_requested: bool = typer.Option(
1344
+ False, "--changes-requested", help="Cases sent back to their authors instead."
1345
+ ),
1346
+ ):
1347
+ """List cases waiting for review, or with --changes-requested, cases awaiting an edit.
1348
+
1349
+ Example: evalkit review queue
1350
+ """
1351
+ client = _client()
1352
+ params: dict = {}
1353
+ if changes_requested:
1354
+ params["state"] = "changes_requested"
1355
+ elif not everyone:
1356
+ params["reviewer"] = reviewer or client.get("/api/auth/me").json()["username"]
1357
+ items = client.get("/api/review/queue", params=params).json()["items"]
1358
+ if not items:
1359
+ typer.echo("nothing waiting")
1360
+ return
1361
+ for item in items:
1362
+ typer.echo(
1363
+ f"{item['dataset']:<20} {item['case_id']:<40} by {item['requested_by']:<16} "
1364
+ f"to {item['requested_to'] or '-':<16} {item['input'][:60]}"
1365
+ )