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/spans.py ADDED
@@ -0,0 +1,360 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Mapping
4
+ from contextlib import asynccontextmanager
5
+ from datetime import UTC, datetime
6
+ from typing import Any, NamedTuple
7
+
8
+ from pydantic import BaseModel
9
+ from sqlalchemy import (
10
+ BigInteger,
11
+ ColumnElement,
12
+ Text,
13
+ and_,
14
+ any_,
15
+ case,
16
+ cast,
17
+ func,
18
+ not_,
19
+ or_,
20
+ select,
21
+ type_coerce,
22
+ update,
23
+ )
24
+ from sqlalchemy.dialects.postgresql import ARRAY, insert
25
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
26
+
27
+ from evalkit.core.traces.mapping import SDK_ROOT_ATTRIBUTE, AttributeKeys
28
+ from evalkit.core.traces.raw import RawSpan
29
+ from evalkit.db import models
30
+ from evalkit.db.runs import RunSource, eval_run_ids
31
+
32
+ UPSERT_CHUNK = 1000
33
+ SPAN_KEY = ("agent_id", "trace_id", "span_id")
34
+
35
+
36
+ class SessionSummary(BaseModel):
37
+ session_id: str | None
38
+ trace_id: str
39
+ title: str | None
40
+ created_at: datetime
41
+ span_count: int
42
+ sdk_root: dict[str, Any] | None = None
43
+
44
+
45
+ def _aware(value: datetime | None) -> datetime | None:
46
+ if value is None or value.tzinfo is not None:
47
+ return value
48
+ return value.replace(tzinfo=UTC)
49
+
50
+
51
+ SPAN_COLUMNS = (
52
+ models.Span.trace_id,
53
+ models.Span.span_id,
54
+ models.Span.parent_span_id,
55
+ models.Span.name,
56
+ models.Span.kind,
57
+ models.Span.start_time,
58
+ models.Span.end_time,
59
+ models.Span.status_code,
60
+ models.Span.status_message,
61
+ models.Span.attributes,
62
+ models.Span.received_at,
63
+ )
64
+ HEAD_COLUMNS = tuple(c for c in SPAN_COLUMNS if c is not models.Span.attributes)
65
+ STREAM_BATCH = 4
66
+ SESSION_ORDER = (models.Span.start_time, models.Span.id)
67
+
68
+
69
+ def _to_raw(row: Mapping[Any, Any]) -> RawSpan:
70
+ start = _aware(row["start_time"])
71
+ assert start is not None
72
+ return RawSpan(
73
+ trace_id=row["trace_id"],
74
+ span_id=row["span_id"],
75
+ parent_span_id=row["parent_span_id"],
76
+ name=row["name"],
77
+ kind=row["kind"],
78
+ start_time=start,
79
+ end_time=_aware(row["end_time"]),
80
+ status_code=row["status_code"],
81
+ status_message=row["status_message"],
82
+ attributes=row["attributes"],
83
+ events=row.get("events", []),
84
+ resource=row.get("resource", {}),
85
+ received_at=_aware(row["received_at"]),
86
+ )
87
+
88
+
89
+ class AttributeDrop(NamedTuple):
90
+ rows: list[int]
91
+ keys: AttributeKeys
92
+
93
+
94
+ class SessionRead:
95
+ def __init__(self, session: AsyncSession, where: ColumnElement[bool]):
96
+ self._session = session
97
+ self._where = where
98
+
99
+ async def probes(self, keys: set[str]) -> list[tuple[int, RawSpan]]:
100
+ ordered = sorted(keys)
101
+ values = [models.Span.attributes[key].label(f"probe_{i}") for i, key in enumerate(ordered)]
102
+ statement = (
103
+ select(models.Span.id, *HEAD_COLUMNS, *values)
104
+ .where(self._where)
105
+ .order_by(*SESSION_ORDER)
106
+ )
107
+ out: list[tuple[int, RawSpan]] = []
108
+ for row in (await self._session.execute(statement)).mappings():
109
+ found = {key: row[f"probe_{i}"] for i, key in enumerate(ordered)}
110
+ attributes = {key: value for key, value in found.items() if value is not None}
111
+ out.append((row["id"], _to_raw({**row, "attributes": attributes})))
112
+ return out
113
+
114
+ def _attributes(self, drop: AttributeDrop) -> ColumnElement[Any]:
115
+ attributes = models.Span.attributes.expression
116
+ if not drop.rows or not (drop.keys.keys or drop.keys.prefixes):
117
+ return attributes
118
+ each = func.jsonb_each(attributes).table_valued("key", "value")
119
+ rebuilt = func.jsonb_object_agg(each.c.key, each.c.value)
120
+ dropped = models.Span.id == any_(cast(drop.rows, ARRAY(BigInteger)))
121
+ removed = or_(
122
+ each.c.key.in_(sorted(drop.keys.keys)),
123
+ *[func.substr(each.c.key, 1, len(p)) == p for p in sorted(drop.keys.prefixes)],
124
+ )
125
+ kept = select(rebuilt).select_from(each).where(not_(removed)).scalar_subquery()
126
+ return type_coerce(case((dropped, kept), else_=attributes), models.Span.attributes.type)
127
+
128
+ async def attribute_bytes(self, drop: AttributeDrop) -> int:
129
+ size = func.octet_length(cast(self._attributes(drop), Text))
130
+ statement = select(func.coalesce(func.sum(size), 0)).where(self._where)
131
+ return int((await self._session.execute(statement)).scalar_one())
132
+
133
+ async def stream(self, drop: AttributeDrop) -> AsyncIterator[RawSpan]:
134
+ statement = (
135
+ select(*HEAD_COLUMNS, self._attributes(drop).label("attributes"))
136
+ .where(self._where)
137
+ .order_by(*SESSION_ORDER)
138
+ .execution_options(yield_per=STREAM_BATCH)
139
+ )
140
+ result = await self._session.stream(statement)
141
+ async for row in result.mappings():
142
+ yield _to_raw(row)
143
+
144
+
145
+ class SpanStore:
146
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
147
+ self._sessions = session_factory
148
+
149
+ async def upsert(
150
+ self, agent_id: int, spans: list[RawSpan], *, external_trace_source_id: int | None = None
151
+ ) -> int:
152
+ if not spans:
153
+ return 0
154
+ roots: dict[str, str | None] = {
155
+ s.trace_id: s.session_id_hint for s in spans if s.parent_span_id is None
156
+ }
157
+ batch_roots = set(roots)
158
+ async with self._sessions() as session:
159
+ unknown = {s.trace_id for s in spans if s.trace_id not in roots}
160
+ if unknown:
161
+ stored = await session.execute(
162
+ select(models.Span.trace_id, models.Span.session_id).where(
163
+ models.Span.agent_id == agent_id,
164
+ models.Span.trace_id.in_(unknown),
165
+ models.Span.parent_span_id.is_(None),
166
+ )
167
+ )
168
+ roots.update({trace_id: session_id for trace_id, session_id in stored.all()})
169
+ now = datetime.now(UTC)
170
+ rows = {
171
+ (span.trace_id, span.span_id): dict(
172
+ agent_id=agent_id,
173
+ trace_id=span.trace_id,
174
+ span_id=span.span_id,
175
+ parent_span_id=span.parent_span_id,
176
+ session_id=roots.get(span.trace_id) or span.session_id_hint,
177
+ name=span.name,
178
+ kind=span.kind,
179
+ start_time=span.start_time,
180
+ end_time=span.end_time,
181
+ status_code=span.status_code,
182
+ status_message=span.status_message,
183
+ attributes=span.attributes,
184
+ events=span.events,
185
+ resource=span.resource,
186
+ received_at=now,
187
+ external_trace_source_id=external_trace_source_id,
188
+ )
189
+ for span in spans
190
+ }
191
+ values = list(rows.values())
192
+ for start in range(0, len(values), UPSERT_CHUNK):
193
+ statement = insert(models.Span).values(values[start : start + UPSERT_CHUNK])
194
+ await session.execute(
195
+ statement.on_conflict_do_update(
196
+ index_elements=list(SPAN_KEY),
197
+ set_={k: statement.excluded[k] for k in values[0] if k not in SPAN_KEY},
198
+ )
199
+ )
200
+ for trace_id in batch_roots:
201
+ session_id = roots[trace_id]
202
+ if session_id:
203
+ await session.execute(
204
+ update(models.Span)
205
+ .where(
206
+ models.Span.agent_id == agent_id,
207
+ models.Span.trace_id == trace_id,
208
+ or_(
209
+ models.Span.session_id.is_(None),
210
+ models.Span.session_id != session_id,
211
+ ),
212
+ )
213
+ .values(session_id=session_id)
214
+ )
215
+ await session.commit()
216
+ return len(spans)
217
+
218
+ def _trace_ids_of(self, agent_id: int, session_id: str):
219
+ return select(models.Span.trace_id).where(
220
+ models.Span.agent_id == agent_id,
221
+ models.Span.session_id == session_id,
222
+ models.Span.parent_span_id.is_(None),
223
+ )
224
+
225
+ def _in_session(self, agent_id: int, session_id: str) -> ColumnElement[bool]:
226
+ return and_(
227
+ models.Span.agent_id == agent_id,
228
+ or_(
229
+ models.Span.session_id == session_id,
230
+ models.Span.trace_id == session_id,
231
+ models.Span.trace_id.in_(self._trace_ids_of(agent_id, session_id)),
232
+ ),
233
+ )
234
+
235
+ @asynccontextmanager
236
+ async def read_session(self, agent_id: int, session_id: str) -> AsyncIterator[SessionRead]:
237
+ async with self._sessions() as session:
238
+ await session.connection(execution_options={"isolation_level": "REPEATABLE READ"})
239
+ yield SessionRead(session, self._in_session(agent_id, session_id))
240
+
241
+ async def stream_session_spans(self, agent_id: int, session_id: str) -> AsyncIterator[RawSpan]:
242
+ statement = (
243
+ select(*SPAN_COLUMNS, models.Span.events, models.Span.resource)
244
+ .where(self._in_session(agent_id, session_id))
245
+ .order_by(*SESSION_ORDER)
246
+ .execution_options(yield_per=STREAM_BATCH)
247
+ )
248
+ async with self._sessions() as session:
249
+ result = await session.stream(statement)
250
+ async for row in result.mappings():
251
+ yield _to_raw(row)
252
+
253
+ async def session_spans(self, agent_id: int, session_id: str) -> list[RawSpan]:
254
+ return [span async for span in self.stream_session_spans(agent_id, session_id)]
255
+
256
+ async def list_sessions(
257
+ self,
258
+ agent_id: int,
259
+ *,
260
+ query: str | None,
261
+ limit: int,
262
+ offset: int,
263
+ since: datetime | None = None,
264
+ until: datetime | None = None,
265
+ linked: RunSource = "all",
266
+ ) -> tuple[list[SessionSummary], int]:
267
+ where = [models.Span.agent_id == agent_id, models.Span.session_id.is_not(None)]
268
+ if query:
269
+ where.append(models.Span.session_id.startswith(query))
270
+ if linked == "eval":
271
+ where.append(models.Span.session_id.in_(eval_run_ids(agent_id)))
272
+ elif linked == "app":
273
+ where.append(models.Span.session_id.not_in(eval_run_ids(agent_id)))
274
+ newest = func.max(models.Span.start_time)
275
+ started = func.min(models.Span.start_time)
276
+ grouped = (
277
+ select(
278
+ models.Span.session_id,
279
+ started.label("started"),
280
+ newest.label("newest"),
281
+ func.count().label("n"),
282
+ )
283
+ .where(*where)
284
+ .group_by(models.Span.session_id)
285
+ )
286
+ if since is not None:
287
+ grouped = grouped.having(started >= since)
288
+ if until is not None:
289
+ grouped = grouped.having(started < until)
290
+ async with self._sessions() as session:
291
+ total = (
292
+ await session.execute(select(func.count()).select_from(grouped.subquery()))
293
+ ).scalar_one()
294
+ page = (
295
+ await session.execute(grouped.order_by(newest.desc()).limit(limit).offset(offset))
296
+ ).all()
297
+ ids = [row.session_id for row in page]
298
+ titles: dict[str, str] = {}
299
+ sdk_roots: dict[str, dict[str, Any]] = {}
300
+ traces: dict[str, str] = {}
301
+ if ids:
302
+ roots = await session.execute(
303
+ select(models.Span.session_id, models.Span.name, models.Span.attributes)
304
+ .where(
305
+ models.Span.agent_id == agent_id,
306
+ models.Span.session_id.in_(ids),
307
+ models.Span.parent_span_id.is_(None),
308
+ )
309
+ .order_by(models.Span.start_time)
310
+ )
311
+ for session_id, name, attributes in roots.all():
312
+ titles.setdefault(session_id, name)
313
+ if attributes.get(SDK_ROOT_ATTRIBUTE):
314
+ sdk_roots.setdefault(session_id, attributes)
315
+ first = await session.execute(
316
+ select(models.Span.session_id, models.Span.trace_id)
317
+ .where(models.Span.agent_id == agent_id, models.Span.session_id.in_(ids))
318
+ .order_by(models.Span.start_time)
319
+ )
320
+ for session_id, trace_id in first.all():
321
+ traces.setdefault(session_id, trace_id)
322
+ return [
323
+ SessionSummary(
324
+ session_id=row.session_id,
325
+ trace_id=traces.get(row.session_id, ""),
326
+ title=titles.get(row.session_id),
327
+ created_at=_aware(row.started) or row.started,
328
+ span_count=row.n,
329
+ sdk_root=sdk_roots.get(row.session_id),
330
+ )
331
+ for row in page
332
+ ], total
333
+
334
+ async def list_unsessioned(
335
+ self, agent_id: int, *, limit: int, offset: int
336
+ ) -> tuple[list[SessionSummary], int]:
337
+ where = [models.Span.agent_id == agent_id, models.Span.session_id.is_(None)]
338
+ started = func.min(models.Span.start_time)
339
+ grouped = (
340
+ select(models.Span.trace_id, started.label("started"), func.count().label("n"))
341
+ .where(*where)
342
+ .group_by(models.Span.trace_id)
343
+ )
344
+ async with self._sessions() as session:
345
+ total = (
346
+ await session.execute(select(func.count()).select_from(grouped.subquery()))
347
+ ).scalar_one()
348
+ page = (
349
+ await session.execute(grouped.order_by(started.desc()).limit(limit).offset(offset))
350
+ ).all()
351
+ return [
352
+ SessionSummary(
353
+ session_id=None,
354
+ trace_id=row.trace_id,
355
+ title=None,
356
+ created_at=_aware(row.started) or row.started,
357
+ span_count=row.n,
358
+ )
359
+ for row in page
360
+ ], total
evalkit/db/store.py ADDED
@@ -0,0 +1,209 @@
1
+ from sqlalchemy import select
2
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
3
+
4
+ from evalkit.contracts.enums import DatasetKind, ExperimentStatus, SyncSource
5
+ from evalkit.core.access import WorkspaceRef
6
+ from evalkit.core.overlays import OverlayBlock
7
+ from evalkit.db import cases, experiments, fixture_blobs, models, runs, scorecard
8
+ from evalkit.db.cases import sync_dataset
9
+ from evalkit.store.base import (
10
+ AgentRunRecord,
11
+ AggregateRows,
12
+ CaseRep,
13
+ CaseView,
14
+ DatasetRow,
15
+ EvaluationRow,
16
+ Experiment,
17
+ IncomingCase,
18
+ IncomingOverlay,
19
+ NewExperiment,
20
+ RunDetail,
21
+ RunForScoring,
22
+ RunRef,
23
+ Selection,
24
+ SyncResult,
25
+ select_cases,
26
+ )
27
+
28
+
29
+ class PgStore:
30
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
31
+ self._sf = session_factory
32
+
33
+ async def aclose(self) -> None:
34
+ return None
35
+
36
+ async def dataset(self, ref: str) -> DatasetRow:
37
+ async with self._sf() as s:
38
+ return await cases.dataset_by_ref(s, ref)
39
+
40
+ async def cases_for(
41
+ self, dataset: str, selection: Selection, *, agent: str | None
42
+ ) -> list[CaseView]:
43
+ async with self._sf() as s:
44
+ row = await cases.dataset_by_ref(s, dataset)
45
+ selected = select_cases(await cases.current_cases(s, row, agent=agent), selection)
46
+ return selected
47
+
48
+ async def create_experiment(self, req: NewExperiment) -> Experiment:
49
+ async with self._sf() as s:
50
+ exp = await experiments.create_experiment(s, req)
51
+ await s.commit()
52
+ return exp
53
+
54
+ async def get_experiment(self, exp_id: int) -> Experiment:
55
+ async with self._sf() as s:
56
+ return await experiments.get_experiment(s, exp_id)
57
+
58
+ async def pinned_cases(self, exp_id: int) -> list[CaseView]:
59
+ async with self._sf() as s:
60
+ return await experiments.pinned_cases(s, exp_id)
61
+
62
+ async def transition(
63
+ self, exp_id: int, to: ExperimentStatus, holder: str | None, error: str | None = None
64
+ ) -> Experiment:
65
+ async with self._sf() as s:
66
+ exp = await experiments.transition(s, exp_id, to, holder, error)
67
+ await s.commit()
68
+ return exp
69
+
70
+ async def renew_lease(self, exp_id: int, holder: str) -> bool:
71
+ async with self._sf() as s:
72
+ renewed = await experiments.renew_lease(s, exp_id, holder)
73
+ await s.commit()
74
+ return renewed
75
+
76
+ async def set_model(self, exp_id: int, model: str) -> None:
77
+ async with self._sf() as s:
78
+ await experiments.set_model(s, exp_id, model)
79
+ await s.commit()
80
+
81
+ async def pending_runs(self, exp_id: int) -> list[CaseRep]:
82
+ async with self._sf() as s:
83
+ return await runs.pending_runs(s, exp_id)
84
+
85
+ async def harness_run_id_used(self, exp_id: int, harness_run_id: str) -> str | None:
86
+ async with self._sf() as s:
87
+ return await runs.harness_run_id_used(s, exp_id, harness_run_id)
88
+
89
+ async def record_run(
90
+ self,
91
+ exp_id: int,
92
+ case_id: str,
93
+ repetition: int,
94
+ run: AgentRunRecord,
95
+ *,
96
+ holder: str | None = None,
97
+ ) -> RunRef:
98
+ async with self._sf() as s:
99
+ ref = await runs.record_run(s, exp_id, case_id, repetition, run, None, holder=holder)
100
+ await s.commit()
101
+ return ref
102
+
103
+ async def runs_to_score(self, exp_id: int, *, force: bool) -> list[RunForScoring]:
104
+ async with self._sf() as s:
105
+ return await runs.runs_to_score(s, exp_id, force=force)
106
+
107
+ async def publish_evaluations(
108
+ self,
109
+ experiment_run_id: int,
110
+ rows: list[EvaluationRow],
111
+ *,
112
+ force: bool,
113
+ holder: str | None = None,
114
+ ) -> None:
115
+ async with self._sf() as s:
116
+ await runs.publish_evaluations(s, experiment_run_id, rows, force=force, holder=holder)
117
+ await s.commit()
118
+
119
+ async def aggregate(self, exp_id: int) -> AggregateRows:
120
+ async with self._sf() as s:
121
+ return await scorecard.aggregate(s, exp_id)
122
+
123
+ async def run_details(self, exp_id: int) -> list[RunDetail]:
124
+ async with self._sf() as s:
125
+ return await runs.run_details(s, exp_id)
126
+
127
+ async def _dataset_id(self, s: AsyncSession, workspace: WorkspaceRef, name: str) -> int | None:
128
+ return await s.scalar(
129
+ select(models.Dataset.id).where(
130
+ models.Dataset.workspace_id == workspace.id, models.Dataset.name == name
131
+ )
132
+ )
133
+
134
+ async def workspace_cases(
135
+ self, workspace: WorkspaceRef, *, exclude_dataset: str | None
136
+ ) -> dict[str, str]:
137
+ async with self._sf() as s:
138
+ excluded = (
139
+ await self._dataset_id(s, workspace, exclude_dataset)
140
+ if exclude_dataset is not None
141
+ else None
142
+ )
143
+ return await cases.workspace_case_index(s, workspace.id, exclude_dataset=excluded)
144
+
145
+ async def overlays_on(
146
+ self, workspace: WorkspaceRef, case_ids: list[str], *, exclude_dataset: str
147
+ ) -> dict[str, list[OverlayBlock]]:
148
+ if not case_ids:
149
+ return {}
150
+ async with self._sf() as s:
151
+ rows = await s.execute(
152
+ select(models.Overlay, models.Dataset.name, models.Case.case_id)
153
+ .join(models.Dataset, models.Dataset.id == models.Overlay.source_dataset_id)
154
+ .join(models.Case, models.Case.id == models.Overlay.target_case)
155
+ .where(
156
+ models.Dataset.workspace_id == workspace.id,
157
+ models.Dataset.name != exclude_dataset,
158
+ models.Case.case_id.in_(case_ids),
159
+ )
160
+ )
161
+ out: dict[str, list[OverlayBlock]] = {}
162
+ for overlay, source, case_id in rows.all():
163
+ out.setdefault(case_id, []).append(
164
+ OverlayBlock(
165
+ source=source,
166
+ agents=list(overlay.agents),
167
+ tags=list(overlay.tags),
168
+ evaluators=overlay.evaluators,
169
+ source_file=overlay.source_file,
170
+ )
171
+ )
172
+ return out
173
+
174
+ async def has_dataset(self, workspace: WorkspaceRef, name: str) -> bool:
175
+ async with self._sf() as s:
176
+ return await self._dataset_id(s, workspace, name) is not None
177
+
178
+ async def known_blobs(self, shas: list[str]) -> set[str]:
179
+ async with self._sf() as s:
180
+ return await fixture_blobs.known_blobs(s, shas)
181
+
182
+ async def sync(
183
+ self,
184
+ *,
185
+ workspace: WorkspaceRef,
186
+ dataset: str,
187
+ kind: DatasetKind,
188
+ incoming: list[IncomingCase],
189
+ overlays: list[IncomingOverlay],
190
+ full: bool,
191
+ actor_id: int,
192
+ source: SyncSource,
193
+ detail: dict,
194
+ ) -> SyncResult:
195
+ async with self._sf() as s:
196
+ result = await sync_dataset(
197
+ s,
198
+ workspace=workspace,
199
+ dataset=dataset,
200
+ kind=kind,
201
+ cases=incoming,
202
+ overlays=overlays,
203
+ full=full,
204
+ actor_id=actor_id,
205
+ source=source,
206
+ detail=detail,
207
+ )
208
+ await s.commit()
209
+ return result
evalkit/db/tokens.py ADDED
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import secrets
5
+ from datetime import datetime
6
+ from typing import cast
7
+
8
+ from pydantic import BaseModel
9
+ from sqlalchemy import CursorResult, delete, select
10
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
11
+
12
+ from evalkit.contracts.enums import TokenKind
13
+ from evalkit.core.workspace import Reference, slug
14
+ from evalkit.db import models
15
+
16
+
17
+ def token_hash(token: str) -> str:
18
+ return hashlib.sha256(token.encode()).hexdigest()
19
+
20
+
21
+ class AgentTokenRecord(BaseModel):
22
+ id: int
23
+ agent: str
24
+ created_by: str
25
+ created_at: datetime
26
+ last_used_at: datetime | None
27
+
28
+
29
+ class AgentTokens:
30
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
31
+ self._sessions = session_factory
32
+
33
+ async def create(self, *, agent_id: int, user_id: int) -> tuple[str, AgentTokenRecord]:
34
+ token = secrets.token_urlsafe(32)
35
+ async with self._sessions() as session:
36
+ row = models.AuthToken(
37
+ user_id=user_id,
38
+ token_hash=token_hash(token),
39
+ expires_at=None,
40
+ kind=TokenKind.AGENT,
41
+ agent_id=agent_id,
42
+ )
43
+ session.add(row)
44
+ await session.commit()
45
+ await session.refresh(row)
46
+ records = await self._records(session, [row])
47
+ return token, records[0]
48
+
49
+ async def _records(
50
+ self, session: AsyncSession, rows: list[models.AuthToken]
51
+ ) -> list[AgentTokenRecord]:
52
+ out: list[AgentTokenRecord] = []
53
+ for row in rows:
54
+ name, workspace = (
55
+ await session.execute(
56
+ select(models.Agent.name, models.Workspace.name)
57
+ .join(models.Workspace, models.Workspace.id == models.Agent.workspace_id)
58
+ .where(models.Agent.id == row.agent_id)
59
+ )
60
+ ).one()
61
+ user = (
62
+ await session.execute(select(models.User).where(models.User.id == row.user_id))
63
+ ).scalar_one()
64
+ out.append(
65
+ AgentTokenRecord(
66
+ id=row.id,
67
+ agent=slug(Reference(name, workspace)),
68
+ created_by=user.username,
69
+ created_at=row.created_at,
70
+ last_used_at=row.last_used_at,
71
+ )
72
+ )
73
+ return out
74
+
75
+ async def list(self, *, user_id: int | None) -> list[AgentTokenRecord]:
76
+ where = [models.AuthToken.kind == TokenKind.AGENT]
77
+ if user_id is not None:
78
+ where.append(models.AuthToken.user_id == user_id)
79
+ async with self._sessions() as session:
80
+ rows = (
81
+ (
82
+ await session.execute(
83
+ select(models.AuthToken).where(*where).order_by(models.AuthToken.id)
84
+ )
85
+ )
86
+ .scalars()
87
+ .all()
88
+ )
89
+ return await self._records(session, list(rows))
90
+
91
+ async def revoke(self, token_id: int, *, user_id: int | None) -> bool:
92
+ where = [models.AuthToken.id == token_id, models.AuthToken.kind == TokenKind.AGENT]
93
+ if user_id is not None:
94
+ where.append(models.AuthToken.user_id == user_id)
95
+ async with self._sessions() as session:
96
+ result = cast(
97
+ CursorResult, await session.execute(delete(models.AuthToken).where(*where))
98
+ )
99
+ await session.commit()
100
+ return bool(result.rowcount)