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,198 @@
1
+ """trajectory_rubrics — checkpoint rubrics graded over the recorded trajectory.
2
+
3
+ One judge call over the full ATIF trajectory: every tool call, every tool
4
+ result and the agent's reasoning, with nothing truncated. Sized against 100
5
+ production sessions, a single eval run renders to ~45k chars at the median and
6
+ under 400k at the worst turn seen; compact ATIF JSON is 7-17% larger, so one
7
+ call fits.
8
+
9
+ score = satisfied / total
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel, Field
19
+
20
+ from evalkit.contracts.atif import Trajectory
21
+ from evalkit.contracts.cases import TrajectoryRubrics
22
+ from evalkit.core.evaluators.base import EvalContext, builtin_evaluator, neutral_skip
23
+ from evalkit.core.fixtures import compose_input
24
+ from evalkit.core.judge import call_judge
25
+ from evalkit.store.base import ScoreRow
26
+
27
+ LOGGER = logging.getLogger(__name__)
28
+
29
+ SCORE_NAME = "trajectory_rubric_score"
30
+
31
+
32
+ class TrajectoryVerdict(BaseModel):
33
+ index: int = Field(
34
+ description="The number this rubric was listed under in the prompt, counting from 1."
35
+ )
36
+ satisfied: bool = Field(
37
+ description="True only when the trajectory or the final answer contain"
38
+ " positive evidence for the rubric."
39
+ )
40
+ evidence: str = Field(
41
+ default="",
42
+ description='Where it shows: "main:4-5" for steps of the root trajectory,'
43
+ ' "<trajectory_id>:7" for a subagent, or "final answer". Empty when not'
44
+ " satisfied.",
45
+ )
46
+ note: str = Field(
47
+ default="",
48
+ description="One line on why this verdict, when not self-evident.",
49
+ )
50
+
51
+
52
+ class TrajectoryJudgeOutput(BaseModel):
53
+ verdicts: list[TrajectoryVerdict]
54
+
55
+
56
+ SYSTEM_PROMPT = """\
57
+ You are an impartial grader for a financial-research agent. You are given the
58
+ question the agent was asked, a numbered list of rubrics describing checkpoints
59
+ its work should show, its recorded trajectory, and its final answer. You return
60
+ one verdict per rubric. You are strict but fair: a rubric is satisfied only when
61
+ the trajectory or the final answer contain positive evidence for it. Silence is
62
+ not satisfaction; absence of contradiction is not satisfaction."""
63
+
64
+ _LEGEND = """\
65
+ The trajectory is an ATIF document. Steps run in step_id order. A tool call's
66
+ result is in that step's observation.results, matched by source_call_id; a
67
+ result with extra.is_error = true is a failed call. Delegated work is in
68
+ subagent_trajectories, each a trajectory of its own.
69
+
70
+ For each rubric return satisfied = true or false.
71
+
72
+ true the trajectory, or the final answer, shows the rubric was met. Cite
73
+ where: "main:4-5" for steps 4-5 of the root trajectory,
74
+ "<trajectory_id>:7" for step 7 of a subagent (use
75
+ "subagent_trajectories[0]:7" when it has no trajectory_id), or
76
+ "final answer".
77
+ false no positive evidence anywhere in the trajectory or the final answer.
78
+
79
+ Match semantically: an action satisfies a rubric regardless of what the tool or
80
+ mechanism behind it is called. A figure satisfies a rubric when it equals the
81
+ rubric's figure after unit conversion, accepting normal rounding unless the
82
+ rubric states its own band. Grade every rubric independently of the others."""
83
+
84
+
85
+ def trajectory_json(trajectory: Trajectory) -> str:
86
+ return json.dumps(
87
+ trajectory.model_dump(exclude_none=True), ensure_ascii=False, separators=(",", ":")
88
+ )
89
+
90
+
91
+ def user_prompt(question: str, rubrics: list[str], trajectory: str, answer: str) -> str:
92
+ numbered = "\n".join(f"{i}. {r}" for i, r in enumerate(rubrics, 1))
93
+ return "\n".join(
94
+ [
95
+ _LEGEND,
96
+ "",
97
+ "QUESTION",
98
+ question,
99
+ "",
100
+ "RUBRICS",
101
+ numbered,
102
+ "",
103
+ "TRAJECTORY (ATIF JSON)",
104
+ trajectory,
105
+ "",
106
+ "FINAL ANSWER",
107
+ answer,
108
+ ]
109
+ )
110
+
111
+
112
+ class IncompleteVerdicts(Exception):
113
+ """The judge did not verdict every rubric exactly once, even after a retry."""
114
+
115
+
116
+ def _missing(total: int, parsed: TrajectoryJudgeOutput) -> list[int]:
117
+ seen: dict[int, int] = {}
118
+ for v in parsed.verdicts:
119
+ seen[v.index] = seen.get(v.index, 0) + 1
120
+ return [i for i in range(1, total + 1) if seen.get(i, 0) != 1]
121
+
122
+
123
+ async def _judge(ctx: EvalContext, base_prompt: str, total: int) -> TrajectoryJudgeOutput:
124
+ current = base_prompt
125
+ missing: list[int] = []
126
+ for attempt in (1, 2):
127
+ parsed, usage = await call_judge(
128
+ ctx.judge_cfg,
129
+ api_key=ctx.judge_api_key,
130
+ system_prompt=SYSTEM_PROMPT,
131
+ user_prompt=current,
132
+ output_type=TrajectoryJudgeOutput,
133
+ session_id=ctx.session_id,
134
+ evaluator_name="trajectory_rubrics",
135
+ )
136
+ ctx.record_judge_usage(usage)
137
+ missing = _missing(total, parsed)
138
+ if not missing:
139
+ return parsed
140
+ LOGGER.warning(
141
+ "evals.trajectory_rubrics.incomplete_verdicts attempt=%s missing=%s", attempt, missing
142
+ )
143
+ current = (
144
+ f"{base_prompt}\n\nYour previous response did not return exactly one verdict for "
145
+ f"rubrics {missing}. Return one verdict per rubric, for every rubric from 1 to {total}."
146
+ )
147
+ raise IncompleteVerdicts(f"judge left rubrics {missing} unverdicted after a retry")
148
+
149
+
150
+ def _explanation(rubrics: list[str], verdicts: list[TrajectoryVerdict]) -> str:
151
+ by_index = {v.index: v for v in verdicts}
152
+ rows = []
153
+ for index, rubric in enumerate(rubrics, 1):
154
+ v = by_index[index]
155
+ rows.append(
156
+ {
157
+ "index": index,
158
+ "rubric": rubric,
159
+ "verdict": "satisfied" if v.satisfied else "unsatisfied",
160
+ "evidence": v.evidence if v.satisfied else "",
161
+ "note": v.note,
162
+ }
163
+ )
164
+ return json.dumps(
165
+ {
166
+ "satisfied": sum(1 for v in verdicts if v.satisfied),
167
+ "total": len(rubrics),
168
+ "verdicts": rows,
169
+ },
170
+ ensure_ascii=False,
171
+ )
172
+
173
+
174
+ @builtin_evaluator(
175
+ "trajectory_rubrics",
176
+ requires={"trajectory", "tool_calls"},
177
+ emits=(SCORE_NAME,),
178
+ uses_judge=True,
179
+ )
180
+ async def trajectory_rubrics(ctx: EvalContext, config: dict[str, Any]) -> list[ScoreRow]:
181
+ cfg = TrajectoryRubrics.model_validate(config)
182
+ if ctx.trajectory_view is None:
183
+ return [neutral_skip(SCORE_NAME, "no trajectory recorded for this run")]
184
+
185
+ trajectory = trajectory_json(ctx.trajectory_view.trajectory)
186
+ question = str(compose_input(ctx.case.input, ctx.case.files).get("question", ""))
187
+ parsed = await _judge(
188
+ ctx, user_prompt(question, cfg.rubrics, trajectory, ctx.answer), len(cfg.rubrics)
189
+ )
190
+ score = sum(1 for v in parsed.verdicts if v.satisfied) / len(cfg.rubrics)
191
+ return [
192
+ ScoreRow(
193
+ name=SCORE_NAME,
194
+ score=score,
195
+ explanation=_explanation(cfg.rubrics, parsed.verdicts),
196
+ annotator_kind="LLM",
197
+ )
198
+ ]
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+ from pydantic import ValidationError
9
+
10
+ from evalkit.contracts.cases import FixtureEntry, load_registry
11
+ from evalkit.core.lint import Finding
12
+ from evalkit.store.base import CaseFile
13
+
14
+
15
+ def sha256_hex(data: bytes) -> str:
16
+ return hashlib.sha256(data).hexdigest()
17
+
18
+
19
+ def load_registries(paths: list[Path]) -> tuple[dict[str, FixtureEntry], list[Finding]]:
20
+ entries: dict[str, FixtureEntry] = {}
21
+ findings: list[Finding] = []
22
+ for path in paths:
23
+ if not path.is_file():
24
+ continue
25
+ try:
26
+ registry = load_registry(path)
27
+ except (ValidationError, yaml.YAMLError) as exc:
28
+ findings.append(
29
+ Finding(severity="error", code="E-SCHEMA", case_id=path.name, message=str(exc))
30
+ )
31
+ continue
32
+ for entry in registry.fixtures:
33
+ if entry.key in entries:
34
+ findings.append(
35
+ Finding(
36
+ severity="error",
37
+ code="E-FIXTURE-DUPLICATE-KEY",
38
+ case_id=entry.key,
39
+ message=f"fixture key {entry.key!r} is declared more than once ({path})",
40
+ )
41
+ )
42
+ continue
43
+ entries[entry.key] = entry
44
+ return entries, findings
45
+
46
+
47
+ ATTACHED_FILES_HEADER = "Attached files:"
48
+
49
+
50
+ def compose_input(case_input: dict[str, Any], files: list[CaseFile]) -> dict[str, Any]:
51
+ out = dict(case_input)
52
+ question = str(case_input.get("question", ""))
53
+ if not files or question.startswith(ATTACHED_FILES_HEADER):
54
+ return out
55
+ lines = [ATTACHED_FILES_HEADER] + [f"- {f.key}: {f.context}" for f in files]
56
+ out["question"] = "\n".join(lines) + "\n\n" + question
57
+ return out
@@ -0,0 +1,227 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import time
5
+ from collections.abc import AsyncIterator, Sequence
6
+ from dataclasses import dataclass
7
+ from typing import IO
8
+ from urllib.parse import quote
9
+
10
+ import httpx
11
+
12
+ from evalkit.contracts.atif import Trajectory, validate_trajectory
13
+ from evalkit.contracts.harness import (
14
+ Capabilities,
15
+ FileList,
16
+ FileRef,
17
+ RunList,
18
+ RunListParams,
19
+ RunMetadata,
20
+ RunStatus,
21
+ )
22
+ from evalkit.core.agents import AgentDefinition
23
+ from evalkit.core.paths import require_safe_path, require_safe_run_id
24
+
25
+ LOGGER = logging.getLogger(__name__)
26
+
27
+ TERMINAL_STATUSES = {"completed", "failed", "cancelled"}
28
+ GET_ATTEMPTS = 3
29
+ STALE_CONNECTION_ERRORS = (httpx.RemoteProtocolError, httpx.ReadError)
30
+ # Shorter than the 5s idle keep-alive of uvicorn and Node, so evalkit drops an
31
+ # idle connection before the harness closes it under an in-flight poll.
32
+ KEEPALIVE_EXPIRY_SECONDS = 2
33
+
34
+
35
+ @dataclass
36
+ class HarnessFile:
37
+ size: int | None
38
+ chunks: AsyncIterator[bytes]
39
+
40
+
41
+ async def _response_chunks(resp: httpx.Response) -> AsyncIterator[bytes]:
42
+ try:
43
+ async for chunk in resp.aiter_bytes():
44
+ yield chunk
45
+ finally:
46
+ await resp.aclose()
47
+
48
+
49
+ class RunTimeout(Exception):
50
+ def __init__(self, run_id: str, last_status: RunStatus | None):
51
+ self.run_id = run_id
52
+ self.last_status = last_status
53
+ super().__init__(f"run {run_id} did not reach a terminal status in time")
54
+
55
+
56
+ def _raise_for_status(resp: httpx.Response) -> None:
57
+ if resp.is_success:
58
+ return
59
+ message = f"harness returned {resp.status_code} for {resp.request.method} {resp.request.url}"
60
+ body = resp.text.strip()
61
+ if body:
62
+ message += f": {body[:1000]}"
63
+ raise httpx.HTTPStatusError(message, request=resp.request, response=resp)
64
+
65
+
66
+ class HarnessClient:
67
+ def __init__(
68
+ self,
69
+ agent: AgentDefinition,
70
+ api_key: str | None,
71
+ *,
72
+ transport: httpx.AsyncBaseTransport | None = None,
73
+ ):
74
+ assert agent.harness_url is not None
75
+ self.agent = agent
76
+ self._client = httpx.AsyncClient(
77
+ base_url=agent.harness_url.rstrip("/"),
78
+ headers={"x-api-key": api_key} if api_key else {},
79
+ timeout=60,
80
+ limits=httpx.Limits(keepalive_expiry=KEEPALIVE_EXPIRY_SECONDS),
81
+ transport=transport,
82
+ )
83
+ self._capabilities: Capabilities | None = None
84
+
85
+ async def aclose(self) -> None:
86
+ await self._client.aclose()
87
+
88
+ async def _get(self, url: str, *, stream: bool = False, **kwargs) -> httpx.Response:
89
+ for attempt in range(1, GET_ATTEMPTS + 1):
90
+ try:
91
+ request = self._client.build_request("GET", url, **kwargs)
92
+ return await self._client.send(request, stream=stream)
93
+ except STALE_CONNECTION_ERRORS as exc:
94
+ if attempt == GET_ATTEMPTS:
95
+ raise
96
+ LOGGER.warning(
97
+ "harness GET %s disconnected (attempt %s/%s), retrying: %r",
98
+ url,
99
+ attempt,
100
+ GET_ATTEMPTS,
101
+ exc,
102
+ )
103
+ raise AssertionError("unreachable")
104
+
105
+ async def capabilities(self) -> Capabilities:
106
+ if self._capabilities is None:
107
+ resp = await self._get("/capabilities")
108
+ _raise_for_status(resp)
109
+ self._capabilities = Capabilities.model_validate(resp.json())
110
+ return self._capabilities
111
+
112
+ async def start_run(
113
+ self,
114
+ input: dict,
115
+ files: Sequence[tuple[str, bytes | IO[bytes]]] | None = None,
116
+ file_refs: list[FileRef] | None = None,
117
+ model: str | None = None,
118
+ metadata: RunMetadata | None = None,
119
+ ) -> str:
120
+ payload: dict = {"input": input}
121
+ if model is not None:
122
+ payload["model"] = model
123
+ if metadata is not None:
124
+ payload["metadata"] = metadata.model_dump(exclude_none=True)
125
+ if file_refs:
126
+ payload["files"] = [ref.model_dump(exclude_none=True) for ref in file_refs]
127
+ if files:
128
+ parts: list[
129
+ tuple[str, tuple[str | None, str | bytes, str] | tuple[str, bytes | IO[bytes]]]
130
+ ] = [("payload", (None, json.dumps(payload), "application/json"))]
131
+ parts += [("files", (name, data)) for name, data in files]
132
+ resp = await self._client.post("/runs", files=parts)
133
+ else:
134
+ resp = await self._client.post("/runs", json=payload)
135
+ _raise_for_status(resp)
136
+ return resp.json()["run_id"]
137
+
138
+ async def status(self, run_id: str) -> RunStatus:
139
+ resp = await self._get(f"/runs/{_run_segment(run_id)}")
140
+ _raise_for_status(resp)
141
+ return RunStatus.model_validate(resp.json())
142
+
143
+ async def poll_until_terminal(self, run_id: str) -> RunStatus:
144
+ deadline = time.monotonic() + self.agent.settings.run_timeout_seconds
145
+ last: RunStatus | None = None
146
+ while time.monotonic() < deadline:
147
+ last = await self.status(run_id)
148
+ if last.status in TERMINAL_STATUSES:
149
+ return last
150
+ await asyncio.sleep(self.agent.settings.poll_interval_seconds)
151
+ caps = await self.capabilities()
152
+ if caps.accepts.stop:
153
+ await self.stop(run_id)
154
+ last = await self._poll_after_stop(run_id, last)
155
+ else:
156
+ LOGGER.warning(
157
+ "run %s timed out and harness does not accept stop — resources may leak",
158
+ run_id,
159
+ )
160
+ raise RunTimeout(run_id, last)
161
+
162
+ async def _poll_after_stop(self, run_id: str, last: RunStatus | None) -> RunStatus | None:
163
+ deadline = time.monotonic() + self.agent.settings.stop_grace_seconds
164
+ while time.monotonic() < deadline:
165
+ last = await self.status(run_id)
166
+ if last.status in TERMINAL_STATUSES:
167
+ return last
168
+ await asyncio.sleep(self.agent.settings.poll_interval_seconds)
169
+ LOGGER.warning(
170
+ "run %s not terminal %ss after stop; fetching its trajectory anyway",
171
+ run_id,
172
+ self.agent.settings.stop_grace_seconds,
173
+ )
174
+ return last
175
+
176
+ async def stop(self, run_id: str) -> None:
177
+ resp = await self._client.post(f"/runs/{_run_segment(run_id)}/stop")
178
+ _raise_for_status(resp)
179
+
180
+ async def trajectory(self, run_id: str) -> Trajectory:
181
+ resp = await self._get(f"/runs/{_run_segment(run_id)}/trajectory")
182
+ _raise_for_status(resp)
183
+ return validate_trajectory(resp.json())
184
+
185
+ async def list_runs(self, params: RunListParams | None = None) -> RunList:
186
+ resp = await self._get("/runs", params=(params or RunListParams()).as_query())
187
+ _raise_for_status(resp)
188
+ return RunList.model_validate(resp.json())
189
+
190
+ async def list_files(self, run_id: str) -> FileList:
191
+ resp = await self._get(f"/runs/{_run_segment(run_id)}/files")
192
+ _raise_for_status(resp)
193
+ return FileList.model_validate(resp.json())
194
+
195
+ async def _file_response(self, run_id: str, path: str) -> httpx.Response:
196
+ return await self._get(f"/runs/{_run_segment(run_id)}/files/{_file_path(path)}")
197
+
198
+ async def find_file(self, run_id: str, path: str) -> bytes | None:
199
+ resp = await self._file_response(run_id, path)
200
+ if resp.status_code == 404:
201
+ return None
202
+ _raise_for_status(resp)
203
+ return resp.content
204
+
205
+ async def get_file(self, run_id: str, path: str) -> bytes:
206
+ resp = await self._file_response(run_id, path)
207
+ _raise_for_status(resp)
208
+ return resp.content
209
+
210
+ async def stream_file(self, run_id: str, path: str) -> HarnessFile:
211
+ url = f"/runs/{_run_segment(run_id)}/files/{_file_path(path)}"
212
+ resp = await self._get(url, stream=True)
213
+ if not resp.is_success:
214
+ await resp.aread()
215
+ await resp.aclose()
216
+ _raise_for_status(resp)
217
+ length = resp.headers.get("content-length")
218
+ size = int(length) if length and "content-encoding" not in resp.headers else None
219
+ return HarnessFile(size=size, chunks=_response_chunks(resp))
220
+
221
+
222
+ def _run_segment(run_id: str) -> str:
223
+ return quote(require_safe_run_id(run_id), safe="")
224
+
225
+
226
+ def _file_path(path: str) -> str:
227
+ return "/".join(quote(segment, safe="") for segment in require_safe_path(path).split("/"))
@@ -0,0 +1,191 @@
1
+ """Live verification of a client's Harness API implementation.
2
+
3
+ Runs the spec's checks and returns a per-capability pass/fail table:
4
+ capabilities shape, the probe input against the declared input_schema, a
5
+ trivial run to a terminal status (carrying one input file where accepted),
6
+ trajectory ATIF validation, then workspace files / run listing / stop where
7
+ declared.
8
+ """
9
+
10
+ from datetime import UTC, datetime, timedelta
11
+
12
+ import httpx
13
+
14
+ from evalkit.contracts.atif import TrajectoryValidationError, subagent_link_warnings
15
+ from evalkit.contracts.harness import FileRef, RunListParams, input_errors
16
+ from evalkit.core.agents import AgentDefinition
17
+ from evalkit.core.blob import BlobStore
18
+ from evalkit.core.harness import HarnessClient, RunTimeout
19
+ from evalkit.core.paths import UnsafePathError
20
+ from evalkit.core.traces.poll import poll_trajectory
21
+ from evalkit.core.traces.sessions import SessionReader
22
+
23
+ CHECK_INPUT = {"question": "Hi. Reply with one word."}
24
+ CHECK_FILE = ("evalkit-check.txt", b"evalkit harness check\n")
25
+ CHECK_FILE_CONTEXT = "A one-line text file evalkit attaches to its harness check."
26
+ CHECK_BLOB_KEY = "checks/evalkit-check.txt"
27
+ IGNORES_TIME_FILTER = "ignores since/until; evalkit filters the runs itself"
28
+
29
+
30
+ def _entry(status: str, detail: str) -> dict:
31
+ return {"status": status, "detail": detail}
32
+
33
+
34
+ async def check_harness(
35
+ agent: AgentDefinition,
36
+ api_key: str | None,
37
+ blob_store: BlobStore,
38
+ *,
39
+ transport: httpx.AsyncBaseTransport | None = None,
40
+ sessions: SessionReader | None = None,
41
+ agent_slug: str | None = None,
42
+ ) -> dict[str, dict]:
43
+ client = HarnessClient(agent, api_key, transport=transport)
44
+ report: dict[str, dict] = {}
45
+ try:
46
+ try:
47
+ caps = await client.capabilities()
48
+ detail = f"contract_version={caps.contract_version} agent={caps.agent}"
49
+ if caps.revision:
50
+ rev = caps.revision[:8]
51
+ detail += f" revision={rev}"
52
+ if caps.branch:
53
+ detail += f" branch={caps.branch}"
54
+ report["capabilities"] = _entry("pass", detail)
55
+ except (httpx.HTTPError, ValueError) as exc:
56
+ report["capabilities"] = _entry("fail", str(exc))
57
+ return report
58
+
59
+ probe = caps.check_input or CHECK_INPUT
60
+ schema_name = "input_schema" if caps.input_schema else "the default input schema"
61
+ if caps.check_input is not None:
62
+ report["input_schema"] = _entry("pass", f"check_input valid against {schema_name}")
63
+ elif errors := input_errors(caps.effective_input_schema, CHECK_INPUT):
64
+ report["input_schema"] = _entry(
65
+ "fail",
66
+ f"default probe {CHECK_INPUT} fails input_schema ({errors[0]}); "
67
+ "declare check_input",
68
+ )
69
+ else:
70
+ report["input_schema"] = _entry("pass", f"default probe valid against {schema_name}")
71
+
72
+ input_files: list[tuple[str, bytes]] | None = None
73
+ input_refs: list[FileRef] | None = None
74
+ if caps.accepts.file_urls:
75
+ await blob_store.put_object(CHECK_BLOB_KEY, CHECK_FILE[1])
76
+ input_refs = [
77
+ FileRef(
78
+ name=CHECK_FILE[0],
79
+ url=await blob_store.object_url(CHECK_BLOB_KEY),
80
+ context=CHECK_FILE_CONTEXT,
81
+ )
82
+ ]
83
+ elif caps.accepts.files:
84
+ input_files = [CHECK_FILE]
85
+ input_refs = [FileRef(name=CHECK_FILE[0], context=CHECK_FILE_CONTEXT)]
86
+ run_id: str | None = None
87
+ try:
88
+ run_id = await client.start_run(probe, files=input_files, file_refs=input_refs)
89
+ status = await client.poll_until_terminal(run_id)
90
+ carried = [
91
+ kind
92
+ for kind, value in (("an answer", status.answer), ("an output", status.output))
93
+ if value
94
+ ]
95
+ if status.status == "completed" and carried:
96
+ report["run"] = _entry(
97
+ "pass", f"run {run_id} completed with {' and '.join(carried)}"
98
+ )
99
+ else:
100
+ report["run"] = _entry("fail", f"run ended {status.status}, error={status.error}")
101
+ except (httpx.HTTPError, RunTimeout) as exc:
102
+ report["run"] = _entry("fail", str(exc))
103
+ except UnsafePathError:
104
+ report["run"] = _entry("fail", f"run id {run_id!r} must be one plain path segment")
105
+ run_id = None
106
+
107
+ if not caps.accepts.file_input:
108
+ report["files"] = _entry("skipped", "accepts.files=false, accepts.file_urls=false")
109
+ elif run_id is None:
110
+ report["files"] = _entry("fail", report["run"]["detail"])
111
+ elif caps.accepts.file_urls:
112
+ report["files"] = _entry("pass", f"{CHECK_FILE[0]} fetched by url")
113
+ else:
114
+ report["files"] = _entry("pass", f"{CHECK_FILE[0]} accepted on the run request")
115
+
116
+ if agent.is_otel:
117
+ report["trajectory"] = _entry("pass", "from traces")
118
+ if run_id is None:
119
+ report["session"] = _entry("fail", "no run to poll a session for")
120
+ elif sessions is None:
121
+ report["session"] = _entry("skipped", "no session reader for this check")
122
+ else:
123
+ doc, session_status = await poll_trajectory(
124
+ sessions,
125
+ agent_slug or agent.ref,
126
+ run_id,
127
+ agent.settings.trajectory_wait_seconds,
128
+ )
129
+ session = await sessions.trajectory(agent_slug or agent.ref, run_id)
130
+ detail = f"{session_status}: session {run_id}, {session.span_count} spans"
131
+ if doc is not None:
132
+ detail += f", {len(doc.get('steps') or [])} steps"
133
+ report["session"] = _entry(
134
+ {"valid": "pass", "partial": "warn"}.get(session_status, "fail"), detail
135
+ )
136
+ elif not caps.provides.trajectory:
137
+ report["trajectory"] = _entry("skipped", "provides.trajectory=false")
138
+ elif run_id is None:
139
+ report["trajectory"] = _entry("fail", "no run to fetch a trajectory for")
140
+ else:
141
+ try:
142
+ trajectory = await client.trajectory(run_id)
143
+ detail = f"valid {trajectory.schema_version} with {len(trajectory.steps)} steps"
144
+ warnings = subagent_link_warnings(trajectory)
145
+ report["trajectory"] = (
146
+ _entry("warn", "; ".join([detail, *warnings]))
147
+ if warnings
148
+ else _entry("pass", detail)
149
+ )
150
+ except (httpx.HTTPError, TrajectoryValidationError) as exc:
151
+ report["trajectory"] = _entry("fail", str(exc))
152
+
153
+ if not caps.provides.workspace:
154
+ report["workspace"] = _entry("skipped", "provides.workspace=false")
155
+ elif run_id is None:
156
+ report["workspace"] = _entry("fail", "no run to list files for")
157
+ else:
158
+ try:
159
+ files = await client.list_files(run_id)
160
+ report["workspace"] = _entry("pass", f"{len(files.files)} file(s) listed")
161
+ except httpx.HTTPError as exc:
162
+ report["workspace"] = _entry("fail", str(exc))
163
+
164
+ if not caps.provides.run_listing:
165
+ report["run_listing"] = _entry("skipped", "provides.run_listing=false")
166
+ else:
167
+ try:
168
+ runs = await client.list_runs(RunListParams(limit=5))
169
+ future = await client.list_runs(
170
+ RunListParams(limit=5, since=datetime.now(UTC) + timedelta(days=1))
171
+ )
172
+ report["run_listing"] = (
173
+ _entry("warn", f"total={runs.total}; {IGNORES_TIME_FILTER}")
174
+ if future.runs
175
+ else _entry("pass", f"total={runs.total}")
176
+ )
177
+ except httpx.HTTPError as exc:
178
+ report["run_listing"] = _entry("fail", str(exc))
179
+
180
+ if not caps.accepts.stop:
181
+ report["stop"] = _entry("skipped", "accepts.stop=false")
182
+ else:
183
+ try:
184
+ stop_run_id = await client.start_run(probe)
185
+ await client.stop(stop_run_id)
186
+ report["stop"] = _entry("pass", f"run {stop_run_id} stopped")
187
+ except (httpx.HTTPError, UnsafePathError) as exc:
188
+ report["stop"] = _entry("fail", str(exc))
189
+ finally:
190
+ await client.aclose()
191
+ return report