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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. evalkit/__init__.py +0 -0
  2. evalkit/api/__init__.py +0 -0
  3. evalkit/api/access.py +74 -0
  4. evalkit/api/agents.py +159 -0
  5. evalkit/api/app.py +360 -0
  6. evalkit/api/auth.py +225 -0
  7. evalkit/api/cli_dist.py +10 -0
  8. evalkit/api/datasets.py +169 -0
  9. evalkit/api/deps.py +79 -0
  10. evalkit/api/download.py +15 -0
  11. evalkit/api/evaluators.py +121 -0
  12. evalkit/api/experiments.py +616 -0
  13. evalkit/api/fixtures.py +95 -0
  14. evalkit/api/harness_check.py +48 -0
  15. evalkit/api/imports.py +180 -0
  16. evalkit/api/limits.py +84 -0
  17. evalkit/api/llm_providers.py +120 -0
  18. evalkit/api/mcp/__init__.py +0 -0
  19. evalkit/api/mcp/auth.py +100 -0
  20. evalkit/api/mcp/descriptions.py +336 -0
  21. evalkit/api/mcp/models.py +259 -0
  22. evalkit/api/mcp/server.py +101 -0
  23. evalkit/api/mcp/tools.py +844 -0
  24. evalkit/api/meta.py +19 -0
  25. evalkit/api/otel.py +350 -0
  26. evalkit/api/pull.py +197 -0
  27. evalkit/api/review.py +271 -0
  28. evalkit/api/runs.py +555 -0
  29. evalkit/api/server_keys.py +44 -0
  30. evalkit/api/tokens.py +51 -0
  31. evalkit/api/trace_imports.py +173 -0
  32. evalkit/api/trace_sources.py +49 -0
  33. evalkit/api/traces.py +196 -0
  34. evalkit/api/users.py +72 -0
  35. evalkit/api/workspaces.py +131 -0
  36. evalkit/bundled/contracts/atif-accepted.json +5 -0
  37. evalkit/bundled/contracts/custom-evaluator-api-v1.json +243 -0
  38. evalkit/bundled/contracts/harness-api-v1.json +938 -0
  39. evalkit/bundled/contracts/span-mapping-v1.json +314 -0
  40. evalkit/bundled/docs/concepts.md +26 -0
  41. evalkit/bundled/docs/config.md +38 -0
  42. evalkit/bundled/docs/harness-api.md +44 -0
  43. evalkit/bundled/docs/install.md +60 -0
  44. evalkit/bundled/docs/login.md +36 -0
  45. evalkit/bundled/docs/mappings.md +136 -0
  46. evalkit/bundled/docs/mcp.md +44 -0
  47. evalkit/bundled/docs/quickstart.md +142 -0
  48. evalkit/bundled/docs/sdk-runs.md +171 -0
  49. evalkit/bundled/docs/tracing.md +171 -0
  50. evalkit/bundled/docs/try-locally.md +288 -0
  51. evalkit/bundled/docs/workspaces.md +45 -0
  52. evalkit/bundled/skills/authoring-eval-cases/SKILL.md +387 -0
  53. evalkit/bundled/skills/authoring-eval-cases/references/overlays.md +83 -0
  54. evalkit/bundled/skills/authoring-eval-cases/references/tool-checks.md +71 -0
  55. evalkit/bundled/skills/connect-traces/SKILL.md +296 -0
  56. evalkit/bundled/skills/connect-traces/references/gen_ai.json +42 -0
  57. evalkit/bundled/skills/connect-traces/references/openinference.json +35 -0
  58. evalkit/bundled/skills/connect-traces/references/span-mapping-schema.md +88 -0
  59. evalkit/bundled/skills/eval-driven-development/SKILL.md +458 -0
  60. evalkit/bundled/skills/eval-driven-development/references/analyze-run.md +97 -0
  61. evalkit/bundled/skills/eval-driven-development/references/diagnose.md +130 -0
  62. evalkit/bundled/skills/eval-driven-development/references/synthesize-cases.md +87 -0
  63. evalkit/bundled/skills/eval-driven-development/references/synthesize.md +165 -0
  64. evalkit/bundled/skills/evalkit-deployment/SKILL.md +332 -0
  65. evalkit/bundled/skills/evalkit-deployment/config.example.yaml +49 -0
  66. evalkit/bundled/skills/evalkit-deployment/docker-compose.yml +51 -0
  67. evalkit/bundled/skills/evalkit-deployment/references/config-and-deploy.md +333 -0
  68. evalkit/bundled/skills/evalkit-deployment/references/harness-api.md +139 -0
  69. evalkit/bundled/skills/evalkit-deployment/try/.env.example +11 -0
  70. evalkit/bundled/skills/evalkit-deployment/try/agent.py +131 -0
  71. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/balance-sheet/cases.yaml +25 -0
  72. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/income-statement/cases.yaml +43 -0
  73. evalkit/bundled/skills/evalkit-deployment/try/config.yaml +17 -0
  74. evalkit/bundled/skills/evalkit-deployment/try/docker-compose.yml +82 -0
  75. evalkit/bundled/skills/evalkit-deployment/try/evalkit.yaml +4 -0
  76. evalkit/bundled/skills/evalkit-deployment/try/mapping.json +42 -0
  77. evalkit/bundled/skills/evalkit-deployment/try/run.py +33 -0
  78. evalkit/bundled/skills/evalkit-usage/SKILL.md +509 -0
  79. evalkit/bundled/skills/evalkit-usage/references/suite-fixtures.md +42 -0
  80. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  81. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  82. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  83. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  84. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  85. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  86. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  87. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  88. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  89. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  90. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  91. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  92. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  93. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  94. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  95. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  96. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  97. evalkit/bundled/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  98. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  99. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  100. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  101. evalkit/bundled/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  102. evalkit/bundled/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  103. evalkit/bundled/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  104. evalkit/bundled/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  105. evalkit/bundled/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  106. evalkit/bundled/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  107. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  108. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  109. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  110. evalkit/bundled/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  111. evalkit/bundled/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  112. evalkit/bundled/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  113. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  114. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  115. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  116. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  117. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  118. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  119. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  120. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  121. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  122. evalkit/bundled/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  123. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  124. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  125. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  126. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  127. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  128. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  129. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  130. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  131. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  132. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  133. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  134. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  135. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  136. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  137. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  138. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  139. evalkit/bundled/ui/assets/abap-08VXUWAP.js +1 -0
  140. evalkit/bundled/ui/assets/apex-BWPQTe0t.js +1 -0
  141. evalkit/bundled/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  142. evalkit/bundled/ui/assets/bat-i0X4ZdIN.js +1 -0
  143. evalkit/bundled/ui/assets/bicep-B5-_aFwp.js +2 -0
  144. evalkit/bundled/ui/assets/cameligo-DMUM7wLl.js +1 -0
  145. evalkit/bundled/ui/assets/clojure-Cm7r79vr.js +1 -0
  146. evalkit/bundled/ui/assets/codicon-DF1abBS2.ttf +0 -0
  147. evalkit/bundled/ui/assets/coffee-Ba7i2nA0.js +1 -0
  148. evalkit/bundled/ui/assets/cpp-C7h46wYY.js +1 -0
  149. evalkit/bundled/ui/assets/csharp-BKxtCVv1.js +1 -0
  150. evalkit/bundled/ui/assets/csp-bTuwJoIa.js +1 -0
  151. evalkit/bundled/ui/assets/css-DIMkf-bt.js +3 -0
  152. evalkit/bundled/ui/assets/css.worker-DwfUZ3fB.js +96 -0
  153. evalkit/bundled/ui/assets/cssMode-C97l1pbB.js +1 -0
  154. evalkit/bundled/ui/assets/cypher-CVaqCwHa.js +1 -0
  155. evalkit/bundled/ui/assets/dart-onAF5SnQ.js +1 -0
  156. evalkit/bundled/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  157. evalkit/bundled/ui/assets/ecl-94qehhXe.js +1 -0
  158. evalkit/bundled/ui/assets/editor-DjpsE8to.css +1 -0
  159. evalkit/bundled/ui/assets/editor.api-NTesIxw5.js +908 -0
  160. evalkit/bundled/ui/assets/editor.worker-CRz__AKW.js +33 -0
  161. evalkit/bundled/ui/assets/elixir-6RTg0lbw.js +1 -0
  162. evalkit/bundled/ui/assets/flow9-C5_-GSwl.js +1 -0
  163. evalkit/bundled/ui/assets/freemarker2-BWqEFagp.js +3 -0
  164. evalkit/bundled/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  165. evalkit/bundled/ui/assets/go-C-y9NEjX.js +1 -0
  166. evalkit/bundled/ui/assets/graphql-fmXr3nnJ.js +1 -0
  167. evalkit/bundled/ui/assets/handlebars-DkIF-whN.js +1 -0
  168. evalkit/bundled/ui/assets/hcl-CpzslTdj.js +1 -0
  169. evalkit/bundled/ui/assets/html-C_rpeQXJ.js +1 -0
  170. evalkit/bundled/ui/assets/html.worker-D9ugpX4y.js +509 -0
  171. evalkit/bundled/ui/assets/htmlMode-B0YIq3DF.js +1 -0
  172. evalkit/bundled/ui/assets/index-Cw-jBH6M.js +736 -0
  173. evalkit/bundled/ui/assets/index-DKtoQZtm.css +1 -0
  174. evalkit/bundled/ui/assets/ini-sBoK_t0W.js +1 -0
  175. evalkit/bundled/ui/assets/java-BEtHBSE6.js +1 -0
  176. evalkit/bundled/ui/assets/javascript-D4EGhYA6.js +1 -0
  177. evalkit/bundled/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  178. evalkit/bundled/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  179. evalkit/bundled/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  180. evalkit/bundled/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  181. evalkit/bundled/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  182. evalkit/bundled/ui/assets/json.worker-CV9fqkUp.js +65 -0
  183. evalkit/bundled/ui/assets/jsonMode-pQjSfIH6.js +7 -0
  184. evalkit/bundled/ui/assets/julia-Bri6UV-V.js +1 -0
  185. evalkit/bundled/ui/assets/kotlin-BOotOW0E.js +1 -0
  186. evalkit/bundled/ui/assets/less-B9JPFI3C.js +2 -0
  187. evalkit/bundled/ui/assets/lexon-CfSJPG6W.js +1 -0
  188. evalkit/bundled/ui/assets/liquid-DbrvoYjD.js +1 -0
  189. evalkit/bundled/ui/assets/lspLanguageFeatures-D0B7cH6y.js +4 -0
  190. evalkit/bundled/ui/assets/lua-CsQS60Ue.js +1 -0
  191. evalkit/bundled/ui/assets/m3-D-oSqn_W.js +1 -0
  192. evalkit/bundled/ui/assets/markdown-Cimd5fb3.js +1 -0
  193. evalkit/bundled/ui/assets/mdx-B0HrujLR.js +1 -0
  194. evalkit/bundled/ui/assets/mips-CIPQ_RoX.js +1 -0
  195. evalkit/bundled/ui/assets/monacoEditor-DsoPncxW.js +68 -0
  196. evalkit/bundled/ui/assets/monacoEditor-n7clI0si.css +1 -0
  197. evalkit/bundled/ui/assets/msdax-DauUninz.js +1 -0
  198. evalkit/bundled/ui/assets/mysql-SOo6toE5.js +1 -0
  199. evalkit/bundled/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  200. evalkit/bundled/ui/assets/pascal-DrH0SRf2.js +1 -0
  201. evalkit/bundled/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  202. evalkit/bundled/ui/assets/perl-oz_6vUea.js +1 -0
  203. evalkit/bundled/ui/assets/pgsql-DTj74zXo.js +1 -0
  204. evalkit/bundled/ui/assets/php-nr791fC2.js +1 -0
  205. evalkit/bundled/ui/assets/pla-CopQ2nXW.js +1 -0
  206. evalkit/bundled/ui/assets/postiats-43DmfD33.js +1 -0
  207. evalkit/bundled/ui/assets/powerquery-D3hlyOfw.js +1 -0
  208. evalkit/bundled/ui/assets/powershell-DmHpPYUd.js +1 -0
  209. evalkit/bundled/ui/assets/protobuf-C531GsRP.js +2 -0
  210. evalkit/bundled/ui/assets/pug-Z5eAx3Zn.js +1 -0
  211. evalkit/bundled/ui/assets/python-CieHVM9n.js +1 -0
  212. evalkit/bundled/ui/assets/qsharp-DkqhCAOL.js +1 -0
  213. evalkit/bundled/ui/assets/r-BwWrilGY.js +1 -0
  214. evalkit/bundled/ui/assets/razor-sLewSMyp.js +1 -0
  215. evalkit/bundled/ui/assets/redis-ClamHrr6.js +1 -0
  216. evalkit/bundled/ui/assets/redshift-DT7zqm-g.js +1 -0
  217. evalkit/bundled/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  218. evalkit/bundled/ui/assets/ruby-DezsRK8O.js +1 -0
  219. evalkit/bundled/ui/assets/rust-DdL9SqIa.js +1 -0
  220. evalkit/bundled/ui/assets/sb-CcwsVR0C.js +1 -0
  221. evalkit/bundled/ui/assets/scala-DHpiXF5c.js +1 -0
  222. evalkit/bundled/ui/assets/scheme-BeGwcela.js +1 -0
  223. evalkit/bundled/ui/assets/scss-gp-XZpBa.js +3 -0
  224. evalkit/bundled/ui/assets/shell-CC2rA5mh.js +1 -0
  225. evalkit/bundled/ui/assets/solidity-BEEn4gHE.js +1 -0
  226. evalkit/bundled/ui/assets/sophia-CRfGWb83.js +1 -0
  227. evalkit/bundled/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  228. evalkit/bundled/ui/assets/sql-NEE52Syq.js +1 -0
  229. evalkit/bundled/ui/assets/st-DbInun42.js +1 -0
  230. evalkit/bundled/ui/assets/swift-Bxkupp3x.js +1 -0
  231. evalkit/bundled/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  232. evalkit/bundled/ui/assets/tcl-DISqw1ZD.js +1 -0
  233. evalkit/bundled/ui/assets/ts.worker-DIAQkoB3.js +67741 -0
  234. evalkit/bundled/ui/assets/tsMode-BcVQ1nu0.js +11 -0
  235. evalkit/bundled/ui/assets/twig-De2hgUGE.js +1 -0
  236. evalkit/bundled/ui/assets/typescript-CUvNWd9p.js +1 -0
  237. evalkit/bundled/ui/assets/typespec-B8J7ngcE.js +1 -0
  238. evalkit/bundled/ui/assets/vb-DV3o63ZY.js +1 -0
  239. evalkit/bundled/ui/assets/wgsl-DpFanUEy.js +298 -0
  240. evalkit/bundled/ui/assets/workers-CwpYQtzp.js +1 -0
  241. evalkit/bundled/ui/assets/xml-C1HnXMdF.js +1 -0
  242. evalkit/bundled/ui/assets/yaml-D7Ey3HQu.js +1 -0
  243. evalkit/bundled/ui/favicon.svg +6 -0
  244. evalkit/bundled/ui/index.html +14 -0
  245. evalkit/cli/__init__.py +0 -0
  246. evalkit/cli/admin_commands.py +590 -0
  247. evalkit/cli/client.py +67 -0
  248. evalkit/cli/init_server.py +165 -0
  249. evalkit/cli/local_run.py +195 -0
  250. evalkit/cli/main.py +1365 -0
  251. evalkit/cli/runs_commands.py +539 -0
  252. evalkit/cli/server_commands.py +130 -0
  253. evalkit/cli/skill_targets.py +43 -0
  254. evalkit/cli/tokens_commands.py +52 -0
  255. evalkit/cli/traces_commands.py +332 -0
  256. evalkit/cli/update.py +85 -0
  257. evalkit/client/__init__.py +0 -0
  258. evalkit/client/agents.py +25 -0
  259. evalkit/client/api.py +193 -0
  260. evalkit/client/blob.py +104 -0
  261. evalkit/client/connection.py +88 -0
  262. evalkit/client/evaluators.py +11 -0
  263. evalkit/client/sessions.py +17 -0
  264. evalkit/client/store.py +139 -0
  265. evalkit/client/summary.py +46 -0
  266. evalkit/client/sync.py +198 -0
  267. evalkit/config.py +227 -0
  268. evalkit/contracts/__init__.py +0 -0
  269. evalkit/contracts/atif.py +115 -0
  270. evalkit/contracts/cases.py +456 -0
  271. evalkit/contracts/custom_evaluator.py +58 -0
  272. evalkit/contracts/enums.py +86 -0
  273. evalkit/contracts/esf.py +120 -0
  274. evalkit/contracts/harness.py +281 -0
  275. evalkit/contracts/notes.json +4 -0
  276. evalkit/contracts/notes.py +7 -0
  277. evalkit/contracts/settings.py +41 -0
  278. evalkit/core/__init__.py +0 -0
  279. evalkit/core/access.py +75 -0
  280. evalkit/core/agents.py +65 -0
  281. evalkit/core/blob.py +46 -0
  282. evalkit/core/capabilities.py +36 -0
  283. evalkit/core/costmap.py +90 -0
  284. evalkit/core/custom_eval.py +50 -0
  285. evalkit/core/data/cost_map.json +81546 -0
  286. evalkit/core/endpoints.py +31 -0
  287. evalkit/core/evaluators/__init__.py +29 -0
  288. evalkit/core/evaluators/base.py +121 -0
  289. evalkit/core/evaluators/gauges.py +84 -0
  290. evalkit/core/evaluators/presentation.py +166 -0
  291. evalkit/core/evaluators/response_time.py +21 -0
  292. evalkit/core/evaluators/rubrics.py +494 -0
  293. evalkit/core/evaluators/tool_trajectory.py +187 -0
  294. evalkit/core/evaluators/trajectory_rubrics.py +198 -0
  295. evalkit/core/fixtures.py +57 -0
  296. evalkit/core/harness.py +227 -0
  297. evalkit/core/harness_check.py +191 -0
  298. evalkit/core/importer.py +257 -0
  299. evalkit/core/judge.py +78 -0
  300. evalkit/core/lint.py +265 -0
  301. evalkit/core/overlays.py +76 -0
  302. evalkit/core/paths.py +25 -0
  303. evalkit/core/promotion.py +94 -0
  304. evalkit/core/pull.py +198 -0
  305. evalkit/core/python_eval.py +59 -0
  306. evalkit/core/remote_evaluators.py +32 -0
  307. evalkit/core/review.py +220 -0
  308. evalkit/core/run_digest.py +421 -0
  309. evalkit/core/runner.py +995 -0
  310. evalkit/core/scorecard.py +255 -0
  311. evalkit/core/traces/__init__.py +0 -0
  312. evalkit/core/traces/convert.py +336 -0
  313. evalkit/core/traces/importer.py +113 -0
  314. evalkit/core/traces/mapping.py +385 -0
  315. evalkit/core/traces/poll.py +38 -0
  316. evalkit/core/traces/raw.py +21 -0
  317. evalkit/core/traces/sessions.py +172 -0
  318. evalkit/core/traces/settle.py +14 -0
  319. evalkit/core/traces/sources/__init__.py +19 -0
  320. evalkit/core/traces/sources/base.py +25 -0
  321. evalkit/core/traces/sources/phoenix.py +121 -0
  322. evalkit/core/trajectory.py +248 -0
  323. evalkit/core/workspace.py +30 -0
  324. evalkit/credentials.py +119 -0
  325. evalkit/db/__init__.py +0 -0
  326. evalkit/db/agents.py +289 -0
  327. evalkit/db/alembic/env.py +90 -0
  328. evalkit/db/alembic/script.py.mako +28 -0
  329. evalkit/db/alembic/versions/0011_baseline.py +1002 -0
  330. evalkit/db/alembic/versions/0012_ticket_token_kind.py +27 -0
  331. evalkit/db/alembic/versions/0013_keys_and_llm_providers.py +67 -0
  332. evalkit/db/cases.py +608 -0
  333. evalkit/db/crypto.py +12 -0
  334. evalkit/db/evaluators.py +156 -0
  335. evalkit/db/experiments.py +363 -0
  336. evalkit/db/fixture_blobs.py +91 -0
  337. evalkit/db/keys.py +78 -0
  338. evalkit/db/llm_providers.py +176 -0
  339. evalkit/db/locks.py +6 -0
  340. evalkit/db/mappings.py +168 -0
  341. evalkit/db/models.py +567 -0
  342. evalkit/db/promotion.py +271 -0
  343. evalkit/db/rekey.py +46 -0
  344. evalkit/db/review.py +250 -0
  345. evalkit/db/review_state.py +50 -0
  346. evalkit/db/runs.py +533 -0
  347. evalkit/db/scorecard.py +300 -0
  348. evalkit/db/session.py +71 -0
  349. evalkit/db/sessions.py +90 -0
  350. evalkit/db/spans.py +360 -0
  351. evalkit/db/store.py +209 -0
  352. evalkit/db/tokens.py +100 -0
  353. evalkit/db/trace_imports.py +308 -0
  354. evalkit/db/trace_sources.py +131 -0
  355. evalkit/db/users.py +186 -0
  356. evalkit/db/workspaces.py +232 -0
  357. evalkit/paths.py +33 -0
  358. evalkit/sdk/__init__.py +29 -0
  359. evalkit/sdk/binding.py +69 -0
  360. evalkit/sdk/evaluators.py +56 -0
  361. evalkit/sdk/runner.py +957 -0
  362. evalkit/sdk/tracing.py +166 -0
  363. evalkit/store/__init__.py +0 -0
  364. evalkit/store/base.py +354 -0
  365. evalkit/store/blob.py +69 -0
  366. evalkit/version.py +44 -0
  367. evalkit_py-0.2.0.dev12.dist-info/METADATA +205 -0
  368. evalkit_py-0.2.0.dev12.dist-info/RECORD +371 -0
  369. evalkit_py-0.2.0.dev12.dist-info/WHEEL +4 -0
  370. evalkit_py-0.2.0.dev12.dist-info/entry_points.txt +2 -0
  371. evalkit_py-0.2.0.dev12.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,173 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ from fastapi import APIRouter, HTTPException, Request
5
+ from pydantic import AwareDatetime, BaseModel, model_validator
6
+
7
+ from evalkit.api.access import ActorDep
8
+ from evalkit.api.deps import AgentStoreDep, HolderDep, MappingStoreDep, SessionDep
9
+ from evalkit.api.experiments import task_holder
10
+ from evalkit.api.traces import agent_or_404, require_otel, require_write
11
+ from evalkit.contracts.enums import TraceImportStatus
12
+ from evalkit.contracts.esf import MappingDocument
13
+ from evalkit.core.agents import AgentDefinition
14
+ from evalkit.core.runner import error_text
15
+ from evalkit.core.traces.importer import IMPORT_STOPPED_ERROR, guarded_import
16
+ from evalkit.core.traces.sources import client_for
17
+ from evalkit.db.mappings import MappingStore
18
+ from evalkit.db.trace_imports import TraceImportStore, TraceImportView
19
+ from evalkit.db.trace_sources import ResolvedTraceSource
20
+
21
+ router = APIRouter(prefix="/api/agents", tags=["traces"])
22
+ LOGGER = logging.getLogger(__name__)
23
+
24
+ ImportTasks = dict[int, tuple[str, asyncio.Task]]
25
+ NO_SESSION_MAPPING = (
26
+ "activate a mapping with session_id before importing a time range; "
27
+ "import single sessions to try mappings first"
28
+ )
29
+
30
+
31
+ class RangeImportRequest(BaseModel):
32
+ source: str
33
+ project: str
34
+ since: AwareDatetime
35
+ until: AwareDatetime
36
+
37
+ @model_validator(mode="after")
38
+ def ordered(self) -> "RangeImportRequest":
39
+ if self.since >= self.until:
40
+ raise ValueError("since must be before until")
41
+ return self
42
+
43
+
44
+ def _imports(request: Request) -> TraceImportStore:
45
+ return request.app.state.trace_imports
46
+
47
+
48
+ async def _session_mapping(mappings: MappingStore, agent: AgentDefinition) -> MappingDocument:
49
+ active = await mappings.active(agent.id)
50
+ document = MappingDocument.model_validate(active.document) if active else None
51
+ if document is None or document.session_id is None:
52
+ raise HTTPException(status_code=409, detail=NO_SESSION_MAPPING)
53
+ return document
54
+
55
+
56
+ def _log_failure(task: asyncio.Task) -> None:
57
+ if task.cancelled():
58
+ return
59
+ exc = task.exception()
60
+ if exc is not None:
61
+ LOGGER.error("trace import task %s failed: %s", task.get_name(), error_text(exc))
62
+
63
+
64
+ def _launch(
65
+ request: Request,
66
+ import_id: int,
67
+ holder: str,
68
+ source: ResolvedTraceSource,
69
+ mapping: MappingDocument,
70
+ ) -> None:
71
+ client = client_for(
72
+ source.provider,
73
+ source.url,
74
+ source.api_key.get_secret_value() if source.api_key else None,
75
+ request.app.state.cfg.otel.import_page_traces,
76
+ )
77
+ store = _imports(request)
78
+ task = asyncio.create_task(
79
+ guarded_import(import_id, holder, client, store, request.app.state.spans, mapping),
80
+ name=f"import:{import_id}",
81
+ )
82
+ registry: ImportTasks = request.app.state.import_tasks
83
+ registry[import_id] = (holder, task)
84
+ task.add_done_callback(lambda t: registry.pop(import_id, None))
85
+ task.add_done_callback(_log_failure)
86
+
87
+
88
+ async def interrupt_imports(store: TraceImportStore, registry: ImportTasks) -> None:
89
+ tracked = list(registry.items())
90
+ for _, (_, task) in tracked:
91
+ task.cancel()
92
+ await asyncio.gather(*(task for _, (_, task) in tracked), return_exceptions=True)
93
+ for import_id, (holder, task) in tracked:
94
+ if task.cancelled():
95
+ await store.finish(import_id, holder, TraceImportStatus.FAILED, IMPORT_STOPPED_ERROR)
96
+
97
+
98
+ @router.post("/{agent}/imports", status_code=202)
99
+ async def start_import(
100
+ agent: str,
101
+ body: RangeImportRequest,
102
+ request: Request,
103
+ agents: AgentStoreDep,
104
+ mappings: MappingStoreDep,
105
+ session: SessionDep,
106
+ actor: ActorDep,
107
+ pod: HolderDep,
108
+ ) -> TraceImportView:
109
+ _, record = await agent_or_404(agents, agent)
110
+ await require_write(session, actor, record)
111
+ require_otel(record)
112
+ mapping = await _session_mapping(mappings, record)
113
+ source = await request.app.state.trace_sources.resolve(body.source)
114
+ holder = task_holder(pod)
115
+ view = await _imports(request).create(
116
+ record.id, source.id, body.project, body.since, body.until, actor.id, holder
117
+ )
118
+ _launch(request, view.id, holder, source, mapping)
119
+ return view
120
+
121
+
122
+ @router.get("/{agent}/imports")
123
+ async def list_imports(
124
+ agent: str, request: Request, agents: AgentStoreDep
125
+ ) -> list[TraceImportView]:
126
+ _, record = await agent_or_404(agents, agent)
127
+ return await _imports(request).recent(record.id)
128
+
129
+
130
+ @router.get("/{agent}/imports/{import_id}")
131
+ async def get_import(
132
+ agent: str, import_id: int, request: Request, agents: AgentStoreDep
133
+ ) -> TraceImportView:
134
+ _, record = await agent_or_404(agents, agent)
135
+ return await _imports(request).get(record.id, import_id)
136
+
137
+
138
+ @router.post("/{agent}/imports/{import_id}/stop")
139
+ async def stop_import(
140
+ agent: str,
141
+ import_id: int,
142
+ request: Request,
143
+ agents: AgentStoreDep,
144
+ session: SessionDep,
145
+ actor: ActorDep,
146
+ ) -> TraceImportView:
147
+ _, record = await agent_or_404(agents, agent)
148
+ await require_write(session, actor, record)
149
+ return await _imports(request).stop(record.id, import_id)
150
+
151
+
152
+ @router.post("/{agent}/imports/{import_id}/continue")
153
+ async def continue_import(
154
+ agent: str,
155
+ import_id: int,
156
+ request: Request,
157
+ agents: AgentStoreDep,
158
+ mappings: MappingStoreDep,
159
+ session: SessionDep,
160
+ actor: ActorDep,
161
+ pod: HolderDep,
162
+ ) -> TraceImportView:
163
+ _, record = await agent_or_404(agents, agent)
164
+ await require_write(session, actor, record)
165
+ require_otel(record)
166
+ mapping = await _session_mapping(mappings, record)
167
+ store = _imports(request)
168
+ current = await store.get(record.id, import_id)
169
+ source = await request.app.state.trace_sources.resolve(current.source)
170
+ holder = task_holder(pod)
171
+ view = await store.restart(record.id, import_id, holder)
172
+ _launch(request, view.id, holder, source, mapping)
173
+ return view
@@ -0,0 +1,49 @@
1
+ from fastapi import APIRouter, Request
2
+ from pydantic import BaseModel
3
+
4
+ from evalkit.api.auth import AdminUser
5
+ from evalkit.contracts.enums import TraceProvider
6
+ from evalkit.db.trace_sources import TraceSourceStore
7
+
8
+ router = APIRouter(prefix="/api/trace-sources", tags=["trace sources"])
9
+
10
+
11
+ def _store(request: Request) -> TraceSourceStore:
12
+ return request.app.state.trace_sources
13
+
14
+
15
+ @router.get("")
16
+ async def list_trace_sources(request: Request, include_archived: bool = False) -> list[dict]:
17
+ rows = await _store(request).list_sources(include_archived=include_archived)
18
+ return [row.model_dump(mode="json") for row in rows]
19
+
20
+
21
+ class CreateTraceSource(BaseModel):
22
+ name: str
23
+ provider: TraceProvider
24
+ url: str
25
+ api_key: str | None = None
26
+
27
+
28
+ @router.post("")
29
+ async def create_trace_source(body: CreateTraceSource, request: Request, admin: AdminUser) -> dict:
30
+ created = await _store(request).create(body.name, body.provider, body.url, body.api_key)
31
+ return created.model_dump(mode="json")
32
+
33
+
34
+ class EditTraceSource(BaseModel):
35
+ url: str | None = None
36
+ api_key: str | None = None
37
+
38
+
39
+ @router.patch("/{name}")
40
+ async def edit_trace_source(
41
+ name: str, body: EditTraceSource, request: Request, admin: AdminUser
42
+ ) -> dict:
43
+ edited = await _store(request).edit(name, url=body.url, api_key=body.api_key)
44
+ return edited.model_dump(mode="json")
45
+
46
+
47
+ @router.post("/{name}/archive")
48
+ async def archive_trace_source(name: str, request: Request, admin: AdminUser) -> dict:
49
+ return (await _store(request).archive(name)).model_dump(mode="json")
evalkit/api/traces.py ADDED
@@ -0,0 +1,196 @@
1
+ import logging
2
+ from collections.abc import AsyncIterator
3
+ from typing import Literal
4
+
5
+ import httpx
6
+ from fastapi import APIRouter, HTTPException, Request
7
+ from fastapi.responses import JSONResponse, Response, StreamingResponse
8
+ from pydantic import BaseModel, ValidationError
9
+ from pydantic_core import to_json
10
+
11
+ from evalkit.api.access import ActorDep, ensure_active, require, workspace_ref
12
+ from evalkit.api.deps import (
13
+ AgentStoreDep,
14
+ MappingStoreDep,
15
+ SessionDep,
16
+ SessionReaderDep,
17
+ SpanStoreDep,
18
+ )
19
+ from evalkit.api.experiments import slug_or_400
20
+ from evalkit.contracts.esf import MappingDocument
21
+ from evalkit.core.access import can_write
22
+ from evalkit.core.agents import AgentDefinition, UnknownAgent, explicit
23
+ from evalkit.core.traces.mapping import session_id_of
24
+ from evalkit.core.traces.raw import RawSpan
25
+ from evalkit.core.traces.sessions import SessionTrajectory
26
+ from evalkit.core.traces.sources import client_for
27
+ from evalkit.core.workspace import Reference
28
+ from evalkit.db.agents import AgentStore
29
+
30
+ router = APIRouter(prefix="/api", tags=["traces"])
31
+ LOGGER = logging.getLogger(__name__)
32
+
33
+
34
+ async def session_too_large(request: Request, exc: Exception) -> JSONResponse:
35
+ LOGGER.warning("refused session read %s: %s", request.url.path, exc)
36
+ return JSONResponse(status_code=413, content={"detail": str(exc)})
37
+
38
+
39
+ async def agent_or_404(agents: AgentStore, agent: str) -> tuple[Reference, AgentDefinition]:
40
+ ref = slug_or_400(agent)
41
+ return ref, await agents.get(explicit(ref))
42
+
43
+
44
+ async def require_write(session: SessionDep, actor: ActorDep, agent: AgentDefinition) -> None:
45
+ ws = await workspace_ref(session, agent.workspace)
46
+ require(can_write(actor, ws), f"you cannot change {agent.ref}'s mappings or traces")
47
+ ensure_active(ws)
48
+
49
+
50
+ def require_otel(agent: AgentDefinition) -> None:
51
+ if not agent.is_otel:
52
+ raise HTTPException(
53
+ status_code=400,
54
+ detail=f"{agent.ref} takes ATIF trajectories; only OTel agents import traces",
55
+ )
56
+
57
+
58
+ @router.get("/agents/{agent}/sessions/{session_id}/trajectory")
59
+ async def session_trajectory(
60
+ agent: str,
61
+ session_id: str,
62
+ sessions: SessionReaderDep,
63
+ mappings: MappingStoreDep,
64
+ mapping: Literal["active", "draft"] = "active",
65
+ esf: bool = False,
66
+ ) -> Response:
67
+ if mapping == "draft":
68
+ record = await sessions.agent(agent)
69
+ if await mappings.draft(record.id) is None:
70
+ raise HTTPException(
71
+ status_code=404,
72
+ detail=f"no draft mapping for {agent}; push one with evalkit traces mapping push",
73
+ )
74
+ result = await sessions.read(agent, session_id, mapping=mapping, include_esf=esf)
75
+ if result is None:
76
+ raise UnknownAgent(agent)
77
+ if result.esf is None:
78
+ return Response(content=to_json(result), media_type="application/json")
79
+ return StreamingResponse(_with_esf(result), media_type="application/json")
80
+
81
+
82
+ async def _with_esf(result: SessionTrajectory) -> AsyncIterator[bytes]:
83
+ esf = result.esf or []
84
+ yield to_json(result, exclude={"esf"})[:-1] + b',"esf":['
85
+ for i, span in enumerate(esf):
86
+ yield (b"," if i else b"") + to_json(span)
87
+ yield b"]}"
88
+
89
+
90
+ async def _json_array(spans: AsyncIterator[RawSpan]) -> AsyncIterator[bytes]:
91
+ yield b"["
92
+ first = True
93
+ async for span in spans:
94
+ yield (b"" if first else b",") + to_json(span)
95
+ first = False
96
+ yield b"]"
97
+
98
+
99
+ @router.get("/agents/{agent}/sessions/{session_id}/spans")
100
+ async def session_spans(
101
+ agent: str, session_id: str, sessions: SessionReaderDep
102
+ ) -> StreamingResponse:
103
+ record = await sessions.agent(agent)
104
+ if record is None:
105
+ raise UnknownAgent(agent)
106
+ return StreamingResponse(
107
+ _json_array(sessions.raw(record, session_id)), media_type="application/json"
108
+ )
109
+
110
+
111
+ @router.get("/agents/{agent}/mappings")
112
+ async def list_mappings(agent: str, agents: AgentStoreDep, mappings: MappingStoreDep) -> list[dict]:
113
+ _, record = await agent_or_404(agents, agent)
114
+ return [m.model_dump(mode="json") for m in await mappings.list(record.id)]
115
+
116
+
117
+ @router.put("/agents/{agent}/mappings/draft")
118
+ async def put_draft(
119
+ agent: str,
120
+ body: dict,
121
+ agents: AgentStoreDep,
122
+ mappings: MappingStoreDep,
123
+ session: SessionDep,
124
+ actor: ActorDep,
125
+ activate: bool = False,
126
+ ) -> dict:
127
+ _, record = await agent_or_404(agents, agent)
128
+ await require_write(session, actor, record)
129
+ try:
130
+ document = MappingDocument.model_validate(body)
131
+ except ValidationError as exc:
132
+ first = exc.errors()[0]
133
+ location = ".".join(str(p) for p in first["loc"])
134
+ raise HTTPException(status_code=400, detail=f"{location}: {first['msg']}") from exc
135
+ draft = await mappings.put_draft(
136
+ record.id, document.model_dump(by_alias=True, exclude_none=True), actor.username
137
+ )
138
+ if activate:
139
+ draft = await mappings.activate(record.id, draft.version)
140
+ return draft.model_dump(mode="json")
141
+
142
+
143
+ @router.post("/agents/{agent}/mappings/{version}/activate")
144
+ async def activate_mapping(
145
+ agent: str,
146
+ version: int,
147
+ agents: AgentStoreDep,
148
+ mappings: MappingStoreDep,
149
+ session: SessionDep,
150
+ actor: ActorDep,
151
+ ) -> dict:
152
+ _, record = await agent_or_404(agents, agent)
153
+ await require_write(session, actor, record)
154
+ try:
155
+ return (await mappings.activate(record.id, version)).model_dump(mode="json")
156
+ except KeyError as exc:
157
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
158
+
159
+
160
+ class ImportRequest(BaseModel):
161
+ source: str
162
+ project: str
163
+ session_id: str
164
+
165
+
166
+ @router.post("/agents/{agent}/sessions/import")
167
+ async def import_session(
168
+ agent: str,
169
+ body: ImportRequest,
170
+ request: Request,
171
+ agents: AgentStoreDep,
172
+ spans: SpanStoreDep,
173
+ mappings: MappingStoreDep,
174
+ session: SessionDep,
175
+ actor: ActorDep,
176
+ ) -> dict:
177
+ _, record = await agent_or_404(agents, agent)
178
+ await require_write(session, actor, record)
179
+ require_otel(record)
180
+ source = await request.app.state.trace_sources.resolve(body.source)
181
+ client = client_for(
182
+ source.provider,
183
+ source.url,
184
+ source.api_key.get_secret_value() if source.api_key else None,
185
+ request.app.state.cfg.otel.import_page_traces,
186
+ )
187
+ try:
188
+ fetched = await client.session_spans(body.project, body.session_id)
189
+ except httpx.HTTPError as exc:
190
+ raise HTTPException(status_code=502, detail=f"{body.source}: {exc}") from exc
191
+ active = await mappings.active(record.id)
192
+ document = MappingDocument.model_validate(active.document) if active else None
193
+ for span in fetched:
194
+ span.session_id_hint = session_id_of(span.attributes, document) or body.session_id
195
+ imported = await spans.upsert(record.id, fetched, external_trace_source_id=source.id)
196
+ return {"imported": imported}
evalkit/api/users.py ADDED
@@ -0,0 +1,72 @@
1
+ from fastapi import APIRouter, Request
2
+ from pydantic import BaseModel
3
+
4
+ from evalkit.api.auth import AdminUser
5
+ from evalkit.db.users import UserStore
6
+
7
+ router = APIRouter(prefix="/api/users", tags=["users"])
8
+
9
+
10
+ def _users(request: Request) -> UserStore:
11
+ return UserStore(request.app.state.session_factory)
12
+
13
+
14
+ @router.get("")
15
+ async def list_users(request: Request, admin: AdminUser) -> list[dict]:
16
+ return [
17
+ {
18
+ "id": u.id,
19
+ "username": u.username,
20
+ "is_admin": u.is_admin,
21
+ "archived": u.archived_at is not None,
22
+ "created_at": u.created_at.isoformat(),
23
+ }
24
+ for u in await _users(request).list()
25
+ ]
26
+
27
+
28
+ class CreateUserRequest(BaseModel):
29
+ username: str
30
+ password: str
31
+
32
+
33
+ @router.post("")
34
+ async def create_user(body: CreateUserRequest, request: Request, admin: AdminUser) -> dict:
35
+ await _users(request).create(body.username, body.password)
36
+ return {"username": body.username}
37
+
38
+
39
+ @router.post("/{username}/archive")
40
+ async def archive_user(username: str, request: Request, admin: AdminUser) -> dict:
41
+ await _users(request).archive(username)
42
+ return {"archived": username}
43
+
44
+
45
+ @router.post("/{username}/restore")
46
+ async def restore_user(username: str, request: Request, admin: AdminUser) -> dict:
47
+ await _users(request).restore(username)
48
+ return {"restored": username}
49
+
50
+
51
+ class SetPasswordRequest(BaseModel):
52
+ password: str
53
+
54
+
55
+ @router.post("/{username}/password")
56
+ async def set_password(
57
+ username: str, body: SetPasswordRequest, request: Request, admin: AdminUser
58
+ ) -> dict:
59
+ await _users(request).set_password(username, body.password)
60
+ return {"password_set": username}
61
+
62
+
63
+ @router.post("/{username}/promote")
64
+ async def promote_user(username: str, request: Request, admin: AdminUser) -> dict:
65
+ await _users(request).set_admin(username, True)
66
+ return {"promoted": username}
67
+
68
+
69
+ @router.post("/{username}/demote")
70
+ async def demote_user(username: str, request: Request, admin: AdminUser) -> dict:
71
+ await _users(request).set_admin(username, False)
72
+ return {"demoted": username}
@@ -0,0 +1,131 @@
1
+ from datetime import UTC, datetime
2
+
3
+ from fastapi import APIRouter, HTTPException, Request
4
+ from pydantic import BaseModel
5
+
6
+ from evalkit.api.access import ActorDep, require
7
+ from evalkit.api.auth import AdminUser
8
+ from evalkit.contracts.enums import WorkspaceRole
9
+ from evalkit.core.access import Actor, WorkspaceRef
10
+ from evalkit.db.workspaces import WorkspaceStore
11
+
12
+ router = APIRouter(prefix="/api/workspaces", tags=["workspaces"])
13
+
14
+
15
+ def _store(request: Request) -> WorkspaceStore:
16
+ return WorkspaceStore(request.app.state.session_factory)
17
+
18
+
19
+ def _view(ws: WorkspaceRef, names: dict[int, str]) -> dict:
20
+ return {
21
+ "id": ws.id,
22
+ "name": ws.name,
23
+ "type": "shared" if ws.is_shared else "personal",
24
+ "owner": ws.name if not ws.is_shared else None,
25
+ "archived": ws.archived,
26
+ "promotes_to": names.get(ws.promotes_to) if ws.promotes_to is not None else None,
27
+ "promoted_from": names.get(ws.promoted_from) if ws.promoted_from is not None else None,
28
+ }
29
+
30
+
31
+ async def _one(store: WorkspaceStore, ws: WorkspaceRef) -> dict:
32
+ linked = [i for i in (ws.promotes_to, ws.promoted_from) if i is not None]
33
+ names = {i: (await store.get_by_id(i)).name for i in linked}
34
+ return _view(ws, names)
35
+
36
+
37
+ @router.get("")
38
+ async def list_workspaces(request: Request, include_archived: bool = False) -> list[dict]:
39
+ rows = await _store(request).list(include_archived=True)
40
+ names = {ws.id: ws.name for ws in rows}
41
+ return [_view(ws, names) for ws in rows if include_archived or not ws.archived]
42
+
43
+
44
+ class CreateWorkspace(BaseModel):
45
+ name: str
46
+
47
+
48
+ @router.post("")
49
+ async def create_workspace(body: CreateWorkspace, request: Request, admin: AdminUser) -> dict:
50
+ store = _store(request)
51
+ return await _one(store, await store.create_shared(body.name, admin.id))
52
+
53
+
54
+ @router.get("/{ws}")
55
+ async def get_workspace(ws: str, request: Request) -> dict:
56
+ store = _store(request)
57
+ return await _one(store, await store.get_by_name(ws))
58
+
59
+
60
+ class UpdateWorkspace(BaseModel):
61
+ name: str | None = None
62
+ promotes_to: str | None = None
63
+
64
+
65
+ @router.patch("/{ws}")
66
+ async def update_workspace(
67
+ ws: str, body: UpdateWorkspace, request: Request, admin: AdminUser
68
+ ) -> dict:
69
+ store = _store(request)
70
+ update: list[str] = []
71
+ if "promotes_to" in body.model_fields_set:
72
+ await store.set_promotes_to(ws, body.promotes_to)
73
+ if body.promotes_to is not None:
74
+ update.append(
75
+ f"CI that syncs into @{body.promotes_to} must sync into @{ws} instead; "
76
+ f"evalkit sync --workspace {body.promotes_to} now fails"
77
+ )
78
+ if body.name is not None:
79
+ await store.rename(ws, body.name)
80
+ update += [
81
+ f"developer evalkit.yaml credentials keys ending in @{ws}",
82
+ f"CI and scripts that pass --workspace {ws} or refer to name@{ws}",
83
+ f"bookmarked UI links that contain @{ws}",
84
+ ]
85
+ current = await store.get_by_name(body.name or ws)
86
+ return {**(await _one(store, current)), "update": update}
87
+
88
+
89
+ def _may_archive(actor: Actor, ws: WorkspaceRef) -> bool:
90
+ return actor.is_admin if ws.is_shared else ws.owner_user_id == actor.id
91
+
92
+
93
+ @router.post("/{ws}/archive")
94
+ async def archive_workspace(ws: str, request: Request, actor: ActorDep) -> dict:
95
+ store = _store(request)
96
+ ref = await store.get_by_name(ws)
97
+ require(_may_archive(actor, ref), "only admins archive shared workspaces, owners their own")
98
+ return await _one(store, await store.set_archived(ws, datetime.now(UTC)))
99
+
100
+
101
+ @router.post("/{ws}/restore")
102
+ async def restore_workspace(ws: str, request: Request, actor: ActorDep) -> dict:
103
+ store = _store(request)
104
+ ref = await store.get_by_name(ws)
105
+ require(_may_archive(actor, ref), "only admins restore shared workspaces, owners their own")
106
+ return await _one(store, await store.set_archived(ws, None))
107
+
108
+
109
+ @router.get("/{ws}/members")
110
+ async def list_members(ws: str, request: Request, role: WorkspaceRole | None = None) -> list[dict]:
111
+ return [m.model_dump(mode="json") for m in await _store(request).members(ws, role)]
112
+
113
+
114
+ class AddMember(BaseModel):
115
+ username: str
116
+ role: WorkspaceRole
117
+
118
+
119
+ @router.post("/{ws}/members")
120
+ async def add_member(ws: str, body: AddMember, request: Request, admin: AdminUser) -> dict:
121
+ await _store(request).add_member(ws, body.username, body.role, admin.id)
122
+ return {"workspace": ws, "username": body.username, "role": body.role}
123
+
124
+
125
+ @router.delete("/{ws}/members/{username}/{role}")
126
+ async def remove_member(
127
+ ws: str, username: str, role: WorkspaceRole, request: Request, admin: AdminUser
128
+ ) -> dict:
129
+ if not await _store(request).remove_member(ws, username, role):
130
+ raise HTTPException(status_code=404, detail=f"{username} is not a {role} of {ws}")
131
+ return {"removed": username, "role": role}
@@ -0,0 +1,5 @@
1
+ {
2
+ "accepted_schema_versions": [
3
+ "ATIF-v1.8"
4
+ ]
5
+ }