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/db/crypto.py ADDED
@@ -0,0 +1,12 @@
1
+ from cryptography.fernet import Fernet
2
+
3
+
4
+ class SecretBox:
5
+ def __init__(self, key: str):
6
+ self._fernet = Fernet(key.encode())
7
+
8
+ def seal(self, plain: str) -> bytes:
9
+ return self._fernet.encrypt(plain.encode())
10
+
11
+ def open(self, blob: bytes) -> str:
12
+ return self._fernet.decrypt(blob).decode()
@@ -0,0 +1,156 @@
1
+ from datetime import UTC, datetime
2
+
3
+ from sqlalchemy import select
4
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
5
+
6
+ from evalkit.contracts.enums import EvaluatorKind
7
+ from evalkit.contracts.settings import RemoteEvaluatorSettings
8
+ from evalkit.core.remote_evaluators import RemoteEvaluator, UnknownEvaluator
9
+ from evalkit.db import models
10
+ from evalkit.db.locks import lock_name
11
+ from evalkit.db.workspaces import workspace_row
12
+
13
+
14
+ class InvalidEvaluator(ValueError):
15
+ pass
16
+
17
+
18
+ class EvaluatorExists(ValueError):
19
+ def __init__(self, ref: str):
20
+ super().__init__(f"evaluator exists: {ref}")
21
+
22
+
23
+ def _view(row: models.RemoteEvaluator, workspace: str) -> RemoteEvaluator:
24
+ return RemoteEvaluator(
25
+ name=row.name,
26
+ workspace=workspace,
27
+ kind=EvaluatorKind(row.kind),
28
+ settings=RemoteEvaluatorSettings.model_validate(row.config),
29
+ archived=row.archived_at is not None,
30
+ has_api_key=row.api_key_encrypted is not None,
31
+ )
32
+
33
+
34
+ class RemoteEvaluatorStore:
35
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
36
+ self._sessions = session_factory
37
+
38
+ async def _row(self, session: AsyncSession, ref: str) -> models.RemoteEvaluator:
39
+ name, _, workspace = ref.partition("@")
40
+ row = await session.scalar(
41
+ select(models.RemoteEvaluator)
42
+ .join(models.Workspace, models.Workspace.id == models.RemoteEvaluator.workspace_id)
43
+ .where(models.RemoteEvaluator.name == name, models.Workspace.name == workspace)
44
+ )
45
+ if row is None:
46
+ raise UnknownEvaluator(ref)
47
+ return row
48
+
49
+ async def get(self, ref: str) -> RemoteEvaluator:
50
+ async with self._sessions() as session:
51
+ return _view(await self._row(session, ref), ref.partition("@")[2])
52
+
53
+ async def create(
54
+ self,
55
+ workspace: str,
56
+ name: str,
57
+ kind: EvaluatorKind,
58
+ settings: RemoteEvaluatorSettings,
59
+ api_key_encrypted: bytes | None = None,
60
+ ) -> RemoteEvaluator:
61
+ if "@" in name or not name:
62
+ raise InvalidEvaluator(f"invalid evaluator name: {name!r}")
63
+ async with self._sessions() as session:
64
+ ws = await workspace_row(session, workspace)
65
+ await lock_name(session, f"evaluator:{ws.id}", name)
66
+ exists = await session.scalar(
67
+ select(models.RemoteEvaluator.id).where(
68
+ models.RemoteEvaluator.workspace_id == ws.id,
69
+ models.RemoteEvaluator.name == name,
70
+ )
71
+ )
72
+ if exists is not None:
73
+ raise EvaluatorExists(f"{name}@{workspace}")
74
+ row = models.RemoteEvaluator(
75
+ workspace_id=ws.id,
76
+ name=name,
77
+ kind=kind.value,
78
+ config=settings.model_dump(),
79
+ api_key_encrypted=api_key_encrypted,
80
+ )
81
+ session.add(row)
82
+ await session.commit()
83
+ return _view(row, workspace)
84
+
85
+ async def edit(
86
+ self,
87
+ ref: str,
88
+ settings: RemoteEvaluatorSettings | None = None,
89
+ *,
90
+ api_key_encrypted: bytes | None = None,
91
+ clear_api_key: bool = False,
92
+ ) -> RemoteEvaluator:
93
+ async with self._sessions() as session:
94
+ row = await self._row(session, ref)
95
+ if settings is not None:
96
+ if settings.endpoint != row.config["endpoint"]:
97
+ row.api_key_encrypted = None
98
+ row.config = settings.model_dump()
99
+ if api_key_encrypted is not None or clear_api_key:
100
+ row.api_key_encrypted = api_key_encrypted
101
+ await session.commit()
102
+ return _view(row, ref.partition("@")[2])
103
+
104
+ async def restore(self, ref: str) -> RemoteEvaluator:
105
+ async with self._sessions() as session:
106
+ row = await self._row(session, ref)
107
+ row.archived_at = None
108
+ await session.commit()
109
+ return _view(row, ref.partition("@")[2])
110
+
111
+ async def sealed_keys(self, workspace: str) -> dict[str, tuple[str, bytes]]:
112
+ query = (
113
+ select(
114
+ models.RemoteEvaluator.name,
115
+ models.RemoteEvaluator.config,
116
+ models.RemoteEvaluator.api_key_encrypted,
117
+ )
118
+ .join(models.Workspace, models.Workspace.id == models.RemoteEvaluator.workspace_id)
119
+ .where(
120
+ models.Workspace.name == workspace,
121
+ models.RemoteEvaluator.api_key_encrypted.is_not(None),
122
+ )
123
+ )
124
+ async with self._sessions() as session:
125
+ return {
126
+ name: (config["endpoint"], sealed)
127
+ for name, config, sealed in (await session.execute(query)).all()
128
+ }
129
+
130
+ async def archive(self, ref: str) -> RemoteEvaluator:
131
+ async with self._sessions() as session:
132
+ row = await self._row(session, ref)
133
+ row.archived_at = datetime.now(UTC)
134
+ await session.commit()
135
+ return _view(row, ref.partition("@")[2])
136
+
137
+ async def in_workspace(self, workspace: str) -> dict[str, RemoteEvaluator]:
138
+ return {e.name: e for e in await self.list_evaluators(workspace=workspace)}
139
+
140
+ async def names(self) -> set[str]:
141
+ return {e.name for e in await self.list_evaluators()}
142
+
143
+ async def list_evaluators(
144
+ self, workspace: str | None = None, include_archived: bool = False
145
+ ) -> list[RemoteEvaluator]:
146
+ query = (
147
+ select(models.RemoteEvaluator, models.Workspace.name)
148
+ .join(models.Workspace, models.Workspace.id == models.RemoteEvaluator.workspace_id)
149
+ .order_by(models.Workspace.name, models.RemoteEvaluator.name)
150
+ )
151
+ if workspace is not None:
152
+ query = query.where(models.Workspace.name == workspace)
153
+ if not include_archived:
154
+ query = query.where(models.RemoteEvaluator.archived_at.is_(None))
155
+ async with self._sessions() as session:
156
+ return [_view(row, ws) for row, ws in (await session.execute(query)).all()]
@@ -0,0 +1,363 @@
1
+ from datetime import UTC, datetime, timedelta
2
+
3
+ from sqlalchemy import Select, func, select, update
4
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
5
+ from sqlalchemy.orm import aliased
6
+
7
+ from evalkit.contracts.enums import ExperimentStatus, RunStatus
8
+ from evalkit.core.access import WorkspaceRef, experiment_workspace
9
+ from evalkit.core.runner import ACTIVE, ALLOWED, LEASE_SECONDS
10
+ from evalkit.core.workspace import parse_slug
11
+ from evalkit.db import models
12
+ from evalkit.db.cases import case_views, dataset_by_ref
13
+ from evalkit.db.workspaces import to_ref, workspace_row
14
+ from evalkit.store.base import (
15
+ CaseView,
16
+ Conflict,
17
+ Experiment,
18
+ ExperimentConfig,
19
+ NewExperiment,
20
+ UnknownExperiment,
21
+ )
22
+
23
+ LEASE = timedelta(seconds=LEASE_SECONDS)
24
+ HomeWorkspace = aliased(models.Workspace)
25
+ DatasetWorkspace = aliased(models.Workspace)
26
+ AgentWorkspace = aliased(models.Workspace)
27
+
28
+
29
+ def _query() -> Select:
30
+ return (
31
+ select(
32
+ models.Experiment,
33
+ HomeWorkspace.name,
34
+ models.Dataset.name,
35
+ DatasetWorkspace.name,
36
+ models.Agent.name,
37
+ AgentWorkspace.name,
38
+ models.User.username,
39
+ )
40
+ .join(HomeWorkspace, HomeWorkspace.id == models.Experiment.workspace_id)
41
+ .join(models.Dataset, models.Dataset.id == models.Experiment.dataset_id)
42
+ .join(DatasetWorkspace, DatasetWorkspace.id == models.Dataset.workspace_id)
43
+ .join(models.Agent, models.Agent.id == models.Experiment.agent_id)
44
+ .join(AgentWorkspace, AgentWorkspace.id == models.Agent.workspace_id)
45
+ .join(models.User, models.User.id == models.Experiment.created_by)
46
+ )
47
+
48
+
49
+ def _to_experiment(row) -> Experiment:
50
+ exp, home, dataset, dataset_ws, agent, agent_ws, created_by = row
51
+ return Experiment(
52
+ id=exp.id,
53
+ number=exp.number,
54
+ workspace=home,
55
+ dataset=f"{dataset}@{dataset_ws}",
56
+ agent=f"{agent}@{agent_ws}",
57
+ agent_id=exp.agent_id,
58
+ created_by=created_by,
59
+ status=ExperimentStatus(exp.status),
60
+ status_error=exp.status_error,
61
+ lease_holder=exp.lease_holder,
62
+ lease_expires_at=exp.lease_expires_at,
63
+ repetitions=exp.repetitions,
64
+ config=ExperimentConfig.model_validate(exp.config),
65
+ started_at=exp.started_at,
66
+ ended_at=exp.ended_at,
67
+ )
68
+
69
+
70
+ async def get_experiment(session: AsyncSession, exp_id: int) -> Experiment:
71
+ row = (await session.execute(_query().where(models.Experiment.id == exp_id))).one_or_none()
72
+ if row is None:
73
+ raise UnknownExperiment(exp_id)
74
+ return _to_experiment(row)
75
+
76
+
77
+ async def list_experiments(
78
+ session: AsyncSession,
79
+ *,
80
+ dataset_id: int | None = None,
81
+ workspace_id: int | None = None,
82
+ status: ExperimentStatus | None = None,
83
+ agent_id: int | None = None,
84
+ ran_case_ids: list[str] | None = None,
85
+ limit: int = 50,
86
+ offset: int = 0,
87
+ ) -> list[Experiment]:
88
+ query = _query()
89
+ if ran_case_ids is not None:
90
+ query = query.where(
91
+ select(models.ExperimentRun.id)
92
+ .join(models.Case, models.Case.id == models.ExperimentRun.case_ref)
93
+ .where(
94
+ models.ExperimentRun.experiment_id == models.Experiment.id,
95
+ models.Case.case_id.in_(ran_case_ids),
96
+ )
97
+ .exists()
98
+ )
99
+ if dataset_id is not None:
100
+ query = query.where(models.Experiment.dataset_id == dataset_id)
101
+ if workspace_id is not None:
102
+ query = query.where(models.Experiment.workspace_id == workspace_id)
103
+ if status is not None:
104
+ query = query.where(models.Experiment.status == status.value)
105
+ if agent_id is not None:
106
+ query = query.where(models.Experiment.agent_id == agent_id)
107
+ rows = await session.execute(
108
+ query.order_by(models.Experiment.id.desc()).limit(limit).offset(offset)
109
+ )
110
+ return [_to_experiment(row) for row in rows.all()]
111
+
112
+
113
+ async def _agent(session: AsyncSession, ref: str) -> tuple[models.Agent, WorkspaceRef]:
114
+ parsed = parse_slug(ref)
115
+ workspace = await workspace_row(session, parsed.workspace)
116
+ agent = await session.scalar(
117
+ select(models.Agent).where(
118
+ models.Agent.workspace_id == workspace.id, models.Agent.name == parsed.name
119
+ )
120
+ )
121
+ if agent is None:
122
+ raise KeyError(f"unknown agent: {ref}")
123
+ return agent, to_ref(workspace)
124
+
125
+
126
+ async def _personal(session: AsyncSession, username: str) -> tuple[int, WorkspaceRef]:
127
+ user = await session.scalar(select(models.User).where(models.User.username == username))
128
+ if user is None:
129
+ raise KeyError(f"unknown user: {username}")
130
+ workspace = await session.scalar(
131
+ select(models.Workspace).where(models.Workspace.owner_user_id == user.id)
132
+ )
133
+ assert workspace is not None
134
+ return user.id, to_ref(workspace)
135
+
136
+
137
+ async def create_experiment(session: AsyncSession, req: NewExperiment) -> Experiment:
138
+ dataset = await dataset_by_ref(session, req.dataset)
139
+ dataset_row = await session.scalar(
140
+ select(models.Dataset)
141
+ .where(models.Dataset.id == dataset.id)
142
+ .with_for_update(key_share=True)
143
+ )
144
+ assert dataset_row is not None
145
+ dataset_ws = to_ref(await workspace_row(session, dataset.workspace))
146
+ agent, agent_ws = await _agent(session, req.agent)
147
+ runner_id, runner_ws = await _personal(session, req.runner)
148
+ home = experiment_workspace(runner_ws, dataset_ws, agent_ws)
149
+ number = await session.scalar(
150
+ select(func.coalesce(func.max(models.Experiment.number), 0) + 1).where(
151
+ models.Experiment.dataset_id == dataset.id
152
+ )
153
+ )
154
+ exp = models.Experiment(
155
+ workspace_id=home.id,
156
+ dataset_id=dataset.id,
157
+ number=number,
158
+ agent_id=agent.id,
159
+ created_by=runner_id,
160
+ status=ExperimentStatus.RUNNING.value,
161
+ lease_holder=req.holder,
162
+ lease_expires_at=func.now() + LEASE,
163
+ repetitions=req.repetitions,
164
+ config=req.config.model_dump(mode="json"),
165
+ )
166
+ session.add(exp)
167
+ await session.flush()
168
+ views = await case_views(
169
+ session,
170
+ dataset_row.workspace_id,
171
+ [(pinned.case_ref, pinned.content_hash) for pinned in req.cases],
172
+ agent=req.agent,
173
+ dataset_id=dataset.id,
174
+ )
175
+ session.add_all(
176
+ models.ExperimentCase(
177
+ experiment_id=exp.id,
178
+ case_ref=view.case_ref,
179
+ content_hash=view.content_hash,
180
+ effective_evaluators=view.effective_evaluators,
181
+ )
182
+ for view in views
183
+ )
184
+ await session.flush()
185
+ return await get_experiment(session, exp.id)
186
+
187
+
188
+ async def _compare_and_set(
189
+ session: AsyncSession,
190
+ exp_id: int,
191
+ to: ExperimentStatus,
192
+ holder: str | None,
193
+ error: str | None,
194
+ ) -> bool:
195
+ allowed_from = [f.value for f, t in ALLOWED if t == to and f is not None]
196
+ active = [s.value for s in ACTIVE]
197
+ live = to in ACTIVE
198
+ result = await session.execute(
199
+ update(models.Experiment)
200
+ .where(
201
+ models.Experiment.id == exp_id,
202
+ models.Experiment.status.in_(allowed_from),
203
+ models.Experiment.status.not_in(active)
204
+ | (models.Experiment.lease_expires_at < func.now())
205
+ | (models.Experiment.lease_holder == holder),
206
+ )
207
+ .values(
208
+ status=to.value,
209
+ status_error=error,
210
+ lease_holder=holder if live else None,
211
+ lease_expires_at=func.now() + LEASE if live else None,
212
+ ended_at=None if live else func.now(),
213
+ updated_at=func.now(),
214
+ )
215
+ .execution_options(synchronize_session=False)
216
+ )
217
+ return result.rowcount == 1 # type: ignore[attr-defined]
218
+
219
+
220
+ async def _has_pending_runs(session: AsyncSession, exp_id: int) -> bool:
221
+ exp = await session.get(models.Experiment, exp_id)
222
+ if exp is None:
223
+ raise UnknownExperiment(exp_id)
224
+ completed = (
225
+ select(func.count())
226
+ .select_from(models.ExperimentRun)
227
+ .join(models.AgentRun, models.AgentRun.id == models.ExperimentRun.agent_run_id)
228
+ .where(
229
+ models.ExperimentRun.experiment_id == exp_id,
230
+ models.AgentRun.status == RunStatus.COMPLETED.value,
231
+ )
232
+ .scalar_subquery()
233
+ )
234
+ pinned = (
235
+ select(func.count())
236
+ .select_from(models.ExperimentCase)
237
+ .where(models.ExperimentCase.experiment_id == exp_id)
238
+ .scalar_subquery()
239
+ )
240
+ found = (await session.execute(select(pinned, completed))).one()
241
+ return found[0] * exp.repetitions > found[1]
242
+
243
+
244
+ async def has_pending_runs(session: AsyncSession, exp_id: int) -> bool:
245
+ return await _has_pending_runs(session, exp_id)
246
+
247
+
248
+ def resumable(exp: Experiment, pending_runs: bool) -> bool:
249
+ if exp.status is ExperimentStatus.FAILED:
250
+ return True
251
+ return exp.status is ExperimentStatus.COMPLETED and pending_runs
252
+
253
+
254
+ async def transition(
255
+ session: AsyncSession,
256
+ exp_id: int,
257
+ to: ExperimentStatus,
258
+ holder: str | None,
259
+ error: str | None = None,
260
+ ) -> Experiment:
261
+ current = await get_experiment(session, exp_id)
262
+ if current.status is to and (
263
+ current.lease_holder == holder if to in ACTIVE else current.status_error == error
264
+ ):
265
+ return current
266
+ if (
267
+ current.status is ExperimentStatus.COMPLETED
268
+ and to is ExperimentStatus.RUNNING
269
+ and not await _has_pending_runs(session, exp_id)
270
+ ):
271
+ raise Conflict(f"experiment {exp_id} has no runs left to resume")
272
+ if not await _compare_and_set(session, exp_id, to, holder, error):
273
+ current = await get_experiment(session, exp_id)
274
+ raise Conflict(
275
+ f"experiment is {current.status.value}, held by {current.lease_holder or 'nobody'}"
276
+ )
277
+ session.expire_all()
278
+ return await get_experiment(session, exp_id)
279
+
280
+
281
+ async def transition_stale(session: AsyncSession, exp_id: int) -> bool:
282
+ holder = await session.scalar(
283
+ select(models.Experiment.lease_holder).where(models.Experiment.id == exp_id)
284
+ )
285
+ return await _compare_and_set(
286
+ session,
287
+ exp_id,
288
+ ExperimentStatus.FAILED,
289
+ None,
290
+ f"runner {holder} stopped responding",
291
+ )
292
+
293
+
294
+ async def renew_lease(session: AsyncSession, exp_id: int, holder: str) -> bool:
295
+ result = await session.execute(
296
+ update(models.Experiment)
297
+ .where(models.Experiment.id == exp_id, models.Experiment.lease_holder == holder)
298
+ .values(lease_expires_at=func.now() + LEASE)
299
+ .execution_options(synchronize_session=False)
300
+ )
301
+ return result.rowcount == 1 # type: ignore[attr-defined]
302
+
303
+
304
+ async def stale_active(session: AsyncSession, now: datetime) -> list[int]:
305
+ rows = await session.scalars(
306
+ select(models.Experiment.id)
307
+ .where(
308
+ models.Experiment.status.in_([s.value for s in ACTIVE]),
309
+ models.Experiment.lease_expires_at < now,
310
+ )
311
+ .order_by(models.Experiment.id)
312
+ )
313
+ return list(rows.all())
314
+
315
+
316
+ async def set_model(session: AsyncSession, exp_id: int, model: str) -> None:
317
+ row = await session.get(models.Experiment, exp_id)
318
+ if row is None:
319
+ raise UnknownExperiment(exp_id)
320
+ row.config = {**row.config, "model": model}
321
+
322
+
323
+ async def pinned_cases(session: AsyncSession, exp_id: int) -> list[CaseView]:
324
+ exp = await session.get(models.Experiment, exp_id)
325
+ if exp is None:
326
+ raise UnknownExperiment(exp_id)
327
+ pinned = (
328
+ await session.scalars(
329
+ select(models.ExperimentCase).where(models.ExperimentCase.experiment_id == exp_id)
330
+ )
331
+ ).all()
332
+ dataset = await session.get(models.Dataset, exp.dataset_id)
333
+ assert dataset is not None
334
+ views = await case_views(
335
+ session,
336
+ dataset.workspace_id,
337
+ [(row.case_ref, row.content_hash) for row in pinned],
338
+ agent=None,
339
+ dataset_id=dataset.id,
340
+ )
341
+ frozen = {row.case_ref: row.effective_evaluators for row in pinned}
342
+ return [
343
+ view.model_copy(update={"effective_evaluators": frozen[view.case_ref]}) for view in views
344
+ ]
345
+
346
+
347
+ async def reap(session_factory: async_sessionmaker[AsyncSession]) -> list[int]:
348
+ async with session_factory() as s:
349
+ reaped = [
350
+ exp_id
351
+ for exp_id in await stale_active(s, datetime.now(UTC))
352
+ if await transition_stale(s, exp_id)
353
+ ]
354
+ await s.commit()
355
+ return reaped
356
+
357
+
358
+ async def experiment_of_run(session: AsyncSession, experiment_run_id: int) -> int | None:
359
+ return await session.scalar(
360
+ select(models.ExperimentRun.experiment_id).where(
361
+ models.ExperimentRun.id == experiment_run_id
362
+ )
363
+ )
@@ -0,0 +1,91 @@
1
+ from sqlalchemy import Select, func, select
2
+ from sqlalchemy.dialects.postgresql import insert
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+
5
+ from evalkit.db import models
6
+ from evalkit.store.base import FixtureUse, FixtureUsePage
7
+
8
+
9
+ async def known_blobs(session: AsyncSession, shas: list[str]) -> set[str]:
10
+ if not shas:
11
+ return set()
12
+ rows = await session.scalars(
13
+ select(models.FixtureBlob.sha256).where(models.FixtureBlob.sha256.in_(shas))
14
+ )
15
+ return set(rows.all())
16
+
17
+
18
+ async def blob_size(session: AsyncSession, sha256: str) -> int | None:
19
+ return await session.scalar(
20
+ select(models.FixtureBlob.size).where(models.FixtureBlob.sha256 == sha256)
21
+ )
22
+
23
+
24
+ async def record_blob(
25
+ session: AsyncSession, sha256: str, size: int, uploaded_by: int | None
26
+ ) -> None:
27
+ await session.execute(
28
+ insert(models.FixtureBlob)
29
+ .values(sha256=sha256, size=size, uploaded_by=uploaded_by)
30
+ .on_conflict_do_nothing(index_elements=["sha256"])
31
+ )
32
+
33
+
34
+ def _uses(workspace_id: int, text: str | None) -> Select:
35
+ query = (
36
+ select(
37
+ models.CaseVersionFile.key,
38
+ models.CaseVersionFile.sha256,
39
+ models.FixtureBlob.size,
40
+ func.array_agg(func.distinct(models.Dataset.name)).label("datasets"),
41
+ models.User.username,
42
+ )
43
+ .join(
44
+ models.DatasetCase,
45
+ (models.DatasetCase.case_ref == models.CaseVersionFile.case_ref)
46
+ & (models.DatasetCase.content_hash == models.CaseVersionFile.content_hash),
47
+ )
48
+ .join(models.Dataset, models.Dataset.id == models.DatasetCase.dataset_id)
49
+ .join(models.FixtureBlob, models.FixtureBlob.sha256 == models.CaseVersionFile.sha256)
50
+ .outerjoin(models.User, models.User.id == models.FixtureBlob.uploaded_by)
51
+ .where(models.Dataset.workspace_id == workspace_id, models.DatasetCase.removed_at.is_(None))
52
+ .group_by(
53
+ models.CaseVersionFile.key,
54
+ models.CaseVersionFile.sha256,
55
+ models.FixtureBlob.size,
56
+ models.User.username,
57
+ )
58
+ )
59
+ if text:
60
+ query = query.where(models.CaseVersionFile.key.icontains(text, autoescape=True))
61
+ return query
62
+
63
+
64
+ async def workspace_fixtures(
65
+ session: AsyncSession,
66
+ workspace_id: int,
67
+ *,
68
+ text: str | None = None,
69
+ limit: int = 50,
70
+ offset: int = 0,
71
+ ) -> FixtureUsePage:
72
+ uses = _uses(workspace_id, text)
73
+ total = await session.scalar(select(func.count()).select_from(uses.subquery()))
74
+ rows = await session.execute(
75
+ uses.order_by(models.CaseVersionFile.key, models.CaseVersionFile.sha256)
76
+ .limit(limit)
77
+ .offset(offset)
78
+ )
79
+ return FixtureUsePage(
80
+ items=[
81
+ FixtureUse(
82
+ key=key,
83
+ sha256=sha256,
84
+ size=size,
85
+ datasets=sorted(datasets),
86
+ uploaded_by=uploaded_by,
87
+ )
88
+ for key, sha256, size, datasets, uploaded_by in rows.all()
89
+ ],
90
+ total=total or 0,
91
+ )
evalkit/db/keys.py ADDED
@@ -0,0 +1,78 @@
1
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
2
+
3
+ from evalkit.core.agents import AgentDefinition
4
+ from evalkit.credentials import Kind
5
+ from evalkit.db.agents import AgentStore
6
+ from evalkit.db.crypto import SecretBox
7
+ from evalkit.db.evaluators import RemoteEvaluatorStore
8
+ from evalkit.db.llm_providers import LlmProviderStore
9
+ from evalkit.db.trace_sources import EncryptionKeyMissing
10
+
11
+
12
+ class StoredKeys:
13
+ def __init__(self, keys: dict[tuple[Kind, str], tuple[str, str]]):
14
+ self._keys = keys
15
+
16
+ def key_for(self, kind: Kind, ref: str, url: str | None = None) -> str | None:
17
+ stored = self._keys.get((kind, ref))
18
+ if stored is None:
19
+ return None
20
+ key, stored_url = stored
21
+ return key if url is None or url == stored_url else None
22
+
23
+ def require_judge_key(self, base_url: str) -> str | None:
24
+ return self.key_for("llm_provider", base_url)
25
+
26
+
27
+ class NoKeysLoaded:
28
+ def key_for(self, kind: Kind, ref: str, url: str | None = None) -> str | None:
29
+ raise RuntimeError("server keys are loaded per experiment with KeyLoader.load")
30
+
31
+ def require_judge_key(self, base_url: str) -> str | None:
32
+ raise RuntimeError("server keys are loaded per experiment with KeyLoader.load")
33
+
34
+
35
+ class KeyLoader:
36
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession], box: SecretBox | None):
37
+ self._agents = AgentStore(session_factory)
38
+ self._evaluators = RemoteEvaluatorStore(session_factory)
39
+ self._providers = LlmProviderStore(session_factory)
40
+ self._box = box
41
+
42
+ def _open(self, sealed: bytes) -> str:
43
+ if self._box is None:
44
+ raise EncryptionKeyMissing()
45
+ return self._box.open(sealed)
46
+
47
+ def seal(self, plain: str | None) -> bytes | None:
48
+ if not plain:
49
+ return None
50
+ if self._box is None:
51
+ raise EncryptionKeyMissing()
52
+ return self._box.seal(plain)
53
+
54
+ async def agent_key(self, agent: AgentDefinition) -> str | None:
55
+ if not agent.workspace_is_shared:
56
+ return None
57
+ sealed = await self._agents.sealed_key(agent.ref)
58
+ return self._open(sealed) if sealed is not None else None
59
+
60
+ async def load(self, agent: AgentDefinition, judge_base_url: str | None) -> StoredKeys:
61
+ keys: dict[tuple[Kind, str], tuple[str, str]] = {}
62
+ if not agent.workspace_is_shared:
63
+ return StoredKeys(keys)
64
+ sealed_agent = await self._agents.sealed_key(agent.ref)
65
+ if sealed_agent is not None and agent.harness_url is not None:
66
+ keys["agent", agent.ref] = (self._open(sealed_agent), agent.harness_url)
67
+ for name, (endpoint, sealed) in (
68
+ await self._evaluators.sealed_keys(agent.workspace)
69
+ ).items():
70
+ keys["evaluator", f"{name}@{agent.workspace}"] = (self._open(sealed), endpoint)
71
+ if judge_base_url is not None:
72
+ sealed_provider = await self._providers.sealed_key(judge_base_url)
73
+ if sealed_provider is not None:
74
+ keys["llm_provider", judge_base_url] = (
75
+ self._open(sealed_provider),
76
+ judge_base_url,
77
+ )
78
+ return StoredKeys(keys)