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,255 @@
1
+ """Aggregation of evaluation rows into the scorecard report.
2
+
3
+ A row's bucket is resolved when it is written (`bucket_for` plus the
4
+ exclusions below); bucket means are computed here. `answer_rubric_score` is the
5
+ `content` bucket's only member; every other name the rubric evaluator
6
+ publishes is a diagnostic on it and stays out of the bucket — recall =
7
+ coverage x accuracy identically, so taking all three would double-count every
8
+ missed rubric; the per-type rates partition the very rubrics the score
9
+ already counts; and citation is a separate axis whose denominator exists only
10
+ on cases that ask for sources.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import statistics
16
+ from collections.abc import Iterable
17
+
18
+ from pydantic import BaseModel, Field
19
+
20
+ from evalkit.store.base import AggregateRows
21
+
22
+ GAUGE_PREFIX = "gauge_"
23
+
24
+ ANSWER_RUBRIC_ORDER: tuple[str, ...] = (
25
+ "answer_rubric_score",
26
+ "answer_rubric_coverage",
27
+ "answer_rubric_accuracy",
28
+ "answer_rubric_factual_retrieval",
29
+ "answer_rubric_derived_computation",
30
+ "answer_rubric_analysis_interpretation",
31
+ "answer_rubric_citation",
32
+ )
33
+
34
+ ANSWER_RUBRIC_DIAGNOSTICS = frozenset(ANSWER_RUBRIC_ORDER) - {"answer_rubric_score"}
35
+
36
+ EXCLUDED_FROM_BUCKETS = ANSWER_RUBRIC_DIAGNOSTICS
37
+
38
+ BUCKET_ORDER: tuple[str, ...] = (
39
+ "content",
40
+ "trajectory",
41
+ "presentation",
42
+ "efficiency",
43
+ "custom",
44
+ )
45
+
46
+ BUCKETS: dict[str, frozenset[str]] = {
47
+ "content": frozenset({"answer_rubric_score"}),
48
+ "trajectory": frozenset(
49
+ {"trajectory_rubric_score", "tool_trajectory_match", "required_tools_called"}
50
+ ),
51
+ "presentation": frozenset({"presentation_judge"}),
52
+ "efficiency": frozenset({"max_response_time"}),
53
+ }
54
+
55
+ _BUCKET_BY_EVALUATOR: dict[str, str] = {
56
+ name: bucket for bucket, members in BUCKETS.items() for name in members
57
+ }
58
+
59
+ # One representative evaluator per bucket the inline score blocks show: every
60
+ # bucket with a single member. `custom` has no fixed member and is left out.
61
+ BUCKET_EVALUATOR = {
62
+ bucket: next(iter(members)) for bucket, members in BUCKETS.items() if len(members) == 1
63
+ }
64
+ COST_GAUGE = "gauge_agent_cost_usd"
65
+
66
+
67
+ def is_gauge(name: str) -> bool:
68
+ return name.startswith(GAUGE_PREFIX)
69
+
70
+
71
+ def gauge_places(name: str) -> int:
72
+ """Display precision for a gauge value — dollar gauges need decimals,
73
+ count/token gauges render as whole numbers."""
74
+ return 4 if name.endswith("_cost_usd") else 0
75
+
76
+
77
+ def bucket_for(name: str, source: str = "builtin") -> str | None:
78
+ if source.startswith("custom:"):
79
+ return "custom"
80
+ return _BUCKET_BY_EVALUATOR.get(name)
81
+
82
+
83
+ def order_evaluator_columns(present: Iterable[str]) -> list[str]:
84
+ present = set(present)
85
+ head = [name for name in ANSWER_RUBRIC_ORDER if name in present]
86
+ tail = sorted(present.difference(head))
87
+ return head + tail
88
+
89
+
90
+ class CaseRow(BaseModel):
91
+ case_id: str
92
+ scores: dict[str, float | None] = Field(default_factory=dict)
93
+ explanations: dict[str, str | None] = Field(default_factory=dict)
94
+ gauges: dict[str, float | None] = Field(default_factory=dict)
95
+ errors: dict[str, str] = Field(default_factory=dict)
96
+ run_errors: list[str] = Field(default_factory=list)
97
+ repetitions: int = 1
98
+ overall: float | None = None
99
+ baseline_overall: float | None = None
100
+ delta: float | None = None
101
+
102
+
103
+ class EvaluatorStat(BaseModel):
104
+ name: str
105
+ score: float | None = None
106
+ cases: int = 0
107
+ errors: int = 0
108
+ bucket: str | None = None
109
+ source: str = "builtin"
110
+
111
+
112
+ class GaugeStat(BaseModel):
113
+ name: str
114
+ mean: float | None = None
115
+ median: float | None = None
116
+ count: int = 0
117
+
118
+
119
+ class Report(BaseModel):
120
+ experiment_id: str
121
+ name: str
122
+ dataset: str
123
+ metadata: dict = Field(default_factory=dict)
124
+ cases: list[CaseRow]
125
+ evaluators: list[EvaluatorStat]
126
+ buckets: dict[str, float | None]
127
+ gauges: list[GaugeStat]
128
+ evaluator_columns: list[str]
129
+ baseline_id: str | None = None
130
+
131
+
132
+ def _mean(values: list[float]) -> float | None:
133
+ return sum(values) / len(values) if values else None
134
+
135
+
136
+ def _aggregate_rows(agg: AggregateRows) -> dict[str, CaseRow]:
137
+ rows = {
138
+ case_id: CaseRow(case_id=case_id, repetitions=n)
139
+ for case_id, n in agg.repetitions_seen.items()
140
+ }
141
+ bucketed: dict[str, list[float]] = {}
142
+ for case_id, name, mean, explanation, bucket in agg.case_scores:
143
+ rows[case_id].scores[name] = mean
144
+ rows[case_id].explanations[name] = explanation
145
+ if bucket is not None:
146
+ bucketed.setdefault(case_id, []).append(mean)
147
+ for case_id, name, mean in agg.case_gauges:
148
+ rows[case_id].gauges[name] = mean
149
+ for case_id, name, error in agg.case_errors:
150
+ rows[case_id].errors[name] = error
151
+ for case_id, error in agg.run_errors:
152
+ rows[case_id].run_errors.append(error)
153
+ for case_id, row in rows.items():
154
+ row.overall = _mean(bucketed.get(case_id, []))
155
+ return rows
156
+
157
+
158
+ def assemble_report(agg: AggregateRows, baseline: AggregateRows | None = None) -> Report:
159
+ exp = agg.experiment
160
+ return _report(
161
+ Report(
162
+ experiment_id=str(exp.id),
163
+ name=f"#{exp.number}",
164
+ dataset=exp.dataset,
165
+ metadata={
166
+ **exp.config.model_dump(mode="json"),
167
+ "agent": exp.agent,
168
+ "created_by": exp.created_by,
169
+ "status": exp.status.value,
170
+ },
171
+ cases=[],
172
+ evaluators=[],
173
+ buckets={},
174
+ gauges=[],
175
+ evaluator_columns=[],
176
+ ),
177
+ _aggregate_rows(agg),
178
+ agg.sources,
179
+ {name: bucket for _, name, _, _, bucket in agg.case_scores},
180
+ None if baseline is None else (str(baseline.experiment.id), _aggregate_rows(baseline)),
181
+ )
182
+
183
+
184
+ def _report(
185
+ header: Report,
186
+ rows: dict[str, CaseRow],
187
+ sources: dict[str, str],
188
+ stored_buckets: dict[str, str | None],
189
+ baseline: tuple[str, dict[str, CaseRow]] | None,
190
+ ) -> Report:
191
+ evaluator_names = {
192
+ name
193
+ for row in rows.values()
194
+ for name in list(row.scores) + list(row.errors)
195
+ if not is_gauge(name)
196
+ }
197
+ evaluators = []
198
+ for name in order_evaluator_columns(evaluator_names):
199
+ case_values = [
200
+ value for row in rows.values() if (value := row.scores.get(name)) is not None
201
+ ]
202
+ evaluators.append(
203
+ EvaluatorStat(
204
+ name=name,
205
+ score=_mean(case_values),
206
+ cases=len(case_values),
207
+ errors=sum(1 for row in rows.values() if name in row.errors),
208
+ bucket=stored_buckets.get(name),
209
+ source=sources.get(name, "builtin"),
210
+ )
211
+ )
212
+
213
+ buckets: dict[str, float | None] = {}
214
+ for bucket in BUCKET_ORDER:
215
+ members = [
216
+ stat.score
217
+ for stat in evaluators
218
+ if stat.bucket == bucket
219
+ and stat.score is not None
220
+ and stat.name not in EXCLUDED_FROM_BUCKETS
221
+ ]
222
+ buckets[bucket] = _mean(members)
223
+
224
+ gauge_names = {name for row in rows.values() for name in row.gauges}
225
+ gauges = []
226
+ for name in sorted(gauge_names):
227
+ values = [value for row in rows.values() if (value := row.gauges.get(name)) is not None]
228
+ gauges.append(
229
+ GaugeStat(
230
+ name=name,
231
+ mean=_mean(values),
232
+ median=statistics.median(values) if values else None,
233
+ count=len(values),
234
+ )
235
+ )
236
+
237
+ baseline_id = None
238
+ if baseline is not None:
239
+ baseline_id, baseline_rows = baseline
240
+ for case_id, row in rows.items():
241
+ base = baseline_rows.get(case_id)
242
+ if base is not None and base.overall is not None and row.overall is not None:
243
+ row.baseline_overall = base.overall
244
+ row.delta = round(row.overall - base.overall, 6)
245
+
246
+ return header.model_copy(
247
+ update={
248
+ "cases": sorted(rows.values(), key=lambda r: r.case_id),
249
+ "evaluators": evaluators,
250
+ "buckets": buckets,
251
+ "gauges": gauges,
252
+ "evaluator_columns": order_evaluator_columns(evaluator_names),
253
+ "baseline_id": baseline_id,
254
+ }
255
+ )
File without changes
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from bisect import bisect_right
5
+ from datetime import datetime
6
+ from typing import Any, NamedTuple
7
+
8
+ from evalkit.contracts.atif import (
9
+ AtifAgent,
10
+ FinalMetrics,
11
+ Observation,
12
+ ObservationResult,
13
+ Step,
14
+ StepMetrics,
15
+ SubagentTrajectoryRef,
16
+ ToolCall,
17
+ Trajectory,
18
+ )
19
+ from evalkit.contracts.esf import EsfSpan, MappingDocument
20
+ from evalkit.core.traces.mapping import gen_ai_messages
21
+
22
+ ATIF_VERSION = "ATIF-v1.8"
23
+
24
+
25
+ def _as_text(value: Any) -> str | None:
26
+ if value is None:
27
+ return None
28
+ if isinstance(value, str):
29
+ return value
30
+ return json.dumps(value, ensure_ascii=False, default=str)
31
+
32
+
33
+ def _as_dict(value: dict[str, Any] | str) -> dict[str, Any]:
34
+ if isinstance(value, dict):
35
+ return value
36
+ try:
37
+ parsed = json.loads(value)
38
+ except ValueError:
39
+ return {"raw": value}
40
+ return parsed if isinstance(parsed, dict) else {"raw": value}
41
+
42
+
43
+ def _output_text(value: Any) -> str | None:
44
+ messages = gen_ai_messages(value)
45
+ if messages is None:
46
+ return _as_text(value)
47
+ texts = [m.text for m in messages if m.role == "assistant" and m.text]
48
+ return texts[-1] if texts else None
49
+
50
+
51
+ class _Tree:
52
+ def __init__(self, spans: list[EsfSpan]):
53
+ self.by_id = {s.span_id: s for s in spans}
54
+ self.children: dict[str | None, list[EsfSpan]] = {}
55
+ for s in sorted(spans, key=lambda x: x.started_at):
56
+ parent = s.parent_id if s.parent_id in self.by_id else None
57
+ self.children.setdefault(parent, []).append(s)
58
+
59
+ def roots(self) -> list[EsfSpan]:
60
+ return self.children.get(None, [])
61
+
62
+ def subtree(self, span_id: str) -> list[EsfSpan]:
63
+ out: list[EsfSpan] = []
64
+ stack = [span_id]
65
+ while stack:
66
+ current = stack.pop()
67
+ for child in self.children.get(current, []):
68
+ out.append(child)
69
+ stack.append(child.span_id)
70
+ return out
71
+
72
+ def ancestors(self, span: EsfSpan) -> list[EsfSpan]:
73
+ out: list[EsfSpan] = []
74
+ parent = span.parent_id
75
+ while parent in self.by_id:
76
+ out.append(self.by_id[parent])
77
+ parent = self.by_id[parent].parent_id
78
+ return out
79
+
80
+
81
+ def _answer_of(root: EsfSpan | None, spans: list[EsfSpan], rule: str) -> str | None:
82
+ if rule == "root":
83
+ return _output_text(root.output) if root is not None else None
84
+ name, _, key = rule.partition(".")
85
+ ordered = sorted(spans, key=lambda x: x.started_at)
86
+ named = next((s for s in ordered if s.name == name), None)
87
+ if named is None:
88
+ return None
89
+ if key and isinstance(named.output, dict):
90
+ return _as_text(named.output.get(key))
91
+ return _output_text(named.output)
92
+
93
+
94
+ def _window(llms: list[EsfSpan], at: datetime) -> int | None:
95
+ index = bisect_right([s.started_at for s in llms], at) - 1
96
+ return index if index >= 0 else None
97
+
98
+
99
+ def _result(
100
+ span: EsfSpan, call_id: str | None, refs: dict[str, list[SubagentTrajectoryRef]]
101
+ ) -> ObservationResult:
102
+ return ObservationResult(
103
+ source_call_id=call_id,
104
+ content=_output_text(span.output) if span.kind == "agent" else _as_text(span.output),
105
+ subagent_trajectory_ref=refs.get(span.span_id),
106
+ extra={"is_error": span.error is not None},
107
+ )
108
+
109
+
110
+ def _place(
111
+ steps: list[Step], outcomes: list[EsfSpan], llms: list[EsfSpan]
112
+ ) -> tuple[dict[int, list[tuple[str | None, EsfSpan]]], list[EsfSpan]]:
113
+ owner = {c.tool_call_id: k for k, step in enumerate(steps) for c in step.tool_calls or []}
114
+ placed: dict[int, list[tuple[str | None, EsfSpan]]] = {}
115
+ claimed: set[str] = set()
116
+ by_time: list[EsfSpan] = []
117
+ for span in sorted(outcomes, key=lambda s: s.started_at):
118
+ call_id = span.tool_call_id
119
+ if call_id and call_id in owner and call_id not in claimed:
120
+ claimed.add(call_id)
121
+ placed.setdefault(owner[call_id], []).append((call_id, span))
122
+ else:
123
+ by_time.append(span)
124
+ pending = {
125
+ k: [c.tool_call_id for c in step.tool_calls or [] if c.tool_call_id not in claimed]
126
+ for k, step in enumerate(steps)
127
+ }
128
+ unplaced: list[EsfSpan] = []
129
+ for span in by_time:
130
+ k = _window(llms, span.started_at)
131
+ if k is None:
132
+ unplaced.append(span)
133
+ continue
134
+ placed.setdefault(k, []).append((pending[k].pop(0) if pending[k] else None, span))
135
+ return placed, unplaced
136
+
137
+
138
+ def _steps(
139
+ llms: list[EsfSpan],
140
+ outcomes: list[EsfSpan],
141
+ refs: dict[str, list[SubagentTrajectoryRef]],
142
+ ) -> tuple[list[Step], list[EsfSpan]]:
143
+ steps: list[Step] = []
144
+ agent_steps: list[Step] = []
145
+ llms = sorted(llms, key=lambda s: s.started_at)
146
+ for i, call in enumerate(llms):
147
+ if i == 0 and call.input_messages:
148
+ for m in call.input_messages:
149
+ if m.role == "user":
150
+ steps.append(Step(step_id=0, source="user", message=m.text or ""))
151
+ elif m.role == "system":
152
+ steps.append(Step(step_id=0, source="system", message=m.text or ""))
153
+ assistant = next(
154
+ (m for m in reversed(call.output_messages or []) if m.role == "assistant"), None
155
+ )
156
+ tool_calls = [
157
+ ToolCall(tool_call_id=c.id, function_name=c.name, arguments=_as_dict(c.arguments))
158
+ for c in (assistant.tool_calls if assistant else None) or []
159
+ ]
160
+ step = Step(
161
+ step_id=0,
162
+ timestamp=call.started_at.isoformat(),
163
+ source="agent",
164
+ message=(assistant.text if assistant else None) or "",
165
+ reasoning_content=assistant.reasoning if assistant else None,
166
+ model_name=call.model,
167
+ metrics=StepMetrics(
168
+ prompt_tokens=call.prompt_tokens,
169
+ completion_tokens=call.completion_tokens,
170
+ cached_tokens=call.cached_tokens,
171
+ cost_usd=call.cost_usd,
172
+ extra={"cache_creation_input_tokens": call.cache_write_tokens}
173
+ if call.cache_write_tokens
174
+ else None,
175
+ ),
176
+ tool_calls=tool_calls or None,
177
+ )
178
+ steps.append(step)
179
+ agent_steps.append(step)
180
+ placed, unplaced = _place(agent_steps, outcomes, llms)
181
+ for k, pairs in placed.items():
182
+ order = {c.tool_call_id: j for j, c in enumerate(agent_steps[k].tool_calls or [])}
183
+ pairs.sort(
184
+ key=lambda p: (order.get(p[0], len(order)) if p[0] else len(order), p[1].started_at)
185
+ )
186
+ agent_steps[k].observation = Observation(
187
+ results=[_result(span, call_id, refs) for call_id, span in pairs]
188
+ )
189
+ return steps, unplaced
190
+
191
+
192
+ def _add[Num: (int, float)](total: Num | None, value: Num | None) -> Num | None:
193
+ if total is None:
194
+ return value
195
+ if value is None:
196
+ return total
197
+ return total + value
198
+
199
+
200
+ def _final_metrics(steps: list[Step], subagents: list[Trajectory]) -> FinalMetrics:
201
+ final = FinalMetrics(total_steps=len(steps))
202
+ for m in (s.metrics for s in steps if s.metrics is not None):
203
+ final.total_prompt_tokens = _add(final.total_prompt_tokens, m.prompt_tokens)
204
+ final.total_completion_tokens = _add(final.total_completion_tokens, m.completion_tokens)
205
+ final.total_cached_tokens = _add(final.total_cached_tokens, m.cached_tokens)
206
+ final.total_cost_usd = _add(final.total_cost_usd, m.cost_usd)
207
+ for sub in (s.final_metrics for s in subagents if s.final_metrics is not None):
208
+ final.total_prompt_tokens = _add(final.total_prompt_tokens, sub.total_prompt_tokens)
209
+ final.total_completion_tokens = _add(
210
+ final.total_completion_tokens, sub.total_completion_tokens
211
+ )
212
+ final.total_cached_tokens = _add(final.total_cached_tokens, sub.total_cached_tokens)
213
+ final.total_cost_usd = _add(final.total_cost_usd, sub.total_cost_usd)
214
+ return final
215
+
216
+
217
+ class _Split(NamedTuple):
218
+ tree: _Tree
219
+ original_root: EsfSpan | None
220
+ main_root: EsfSpan | None
221
+ top_nested: list[EsfSpan]
222
+ main: list[EsfSpan]
223
+
224
+
225
+ def _split(kept: list[EsfSpan]) -> _Split:
226
+ tree = _Tree(kept)
227
+ roots = tree.roots()
228
+ original_root = roots[0] if len(roots) == 1 else None
229
+ main_root = original_root
230
+ if main_root is None or main_root.kind != "agent":
231
+ top_agents = [
232
+ s
233
+ for s in kept
234
+ if s.kind == "agent" and not any(a.kind == "agent" for a in tree.ancestors(s))
235
+ ]
236
+ if top_agents:
237
+ main_root = min(top_agents, key=lambda s: s.started_at)
238
+ nested_agent_ids = {s.span_id for s in kept if s.kind == "agent" and s is not main_root}
239
+ top_nested = [
240
+ s
241
+ for s in kept
242
+ if s.span_id in nested_agent_ids
243
+ and not any(a.span_id in nested_agent_ids for a in tree.ancestors(s))
244
+ ]
245
+ nested_ids = {n.span_id for n in top_nested}
246
+ nested_ids |= {s.span_id for n in top_nested for s in tree.subtree(n.span_id)}
247
+ main = [s for s in kept if s.span_id not in nested_ids]
248
+ return _Split(tree, original_root, main_root, top_nested, main)
249
+
250
+
251
+ def _main_llms(main: list[EsfSpan]) -> list[EsfSpan]:
252
+ return [s for s in main if s.kind == "llm"]
253
+
254
+
255
+ def _excluded(mapping: MappingDocument | None) -> set[str]:
256
+ return set(mapping.exclude_spans) if mapping is not None else set()
257
+
258
+
259
+ def first_llm_spans(spans: list[EsfSpan], mapping: MappingDocument | None) -> list[EsfSpan]:
260
+ split = _split([s for s in spans if s.name not in _excluded(mapping)])
261
+ llms = sorted(_main_llms(split.main), key=lambda s: s.started_at)
262
+ nested = [
263
+ first
264
+ for n in split.top_nested
265
+ for first in first_llm_spans([n, *split.tree.subtree(n.span_id)], mapping)
266
+ ]
267
+ return llms[:1] + nested
268
+
269
+
270
+ def to_atif(
271
+ spans: list[EsfSpan],
272
+ mapping: MappingDocument | None,
273
+ *,
274
+ agent_name: str,
275
+ session_id: str | None,
276
+ mapping_version: int | None,
277
+ incomplete_ended_at: datetime | None = None,
278
+ ) -> Trajectory:
279
+ kept = [s for s in spans if s.name not in _excluded(mapping)]
280
+ incomplete = sum(1 for s in kept if s.ended_at is None)
281
+ if incomplete_ended_at is not None:
282
+ kept = [
283
+ s.model_copy(update={"ended_at": incomplete_ended_at}) if s.ended_at is None else s
284
+ for s in kept
285
+ ]
286
+ split = _split(kept)
287
+ tree, original_root, main_root = split.tree, split.original_root, split.main_root
288
+ top_nested, main = split.top_nested, split.main
289
+ tools = [
290
+ s for s in main if s.kind == "tool" and not any(a.kind == "tool" for a in tree.ancestors(s))
291
+ ]
292
+ tool_ids = {t.span_id for t in tools}
293
+ refs: dict[str, list[SubagentTrajectoryRef]] = {}
294
+ outcomes = list(tools)
295
+ for n in top_nested:
296
+ ref = SubagentTrajectoryRef(trajectory_id=f"{n.trace_id}:{n.span_id}")
297
+ spawner = next((a for a in tree.ancestors(n) if a.span_id in tool_ids), n)
298
+ refs.setdefault(spawner.span_id, []).append(ref)
299
+ if spawner is n:
300
+ outcomes.append(n)
301
+ steps, unplaced = _steps(_main_llms(main), outcomes, refs)
302
+ rule = mapping.answer if mapping is not None else "root"
303
+ answer = _answer_of(original_root or main_root, kept, rule)
304
+ if answer and (not steps or steps[-1].tool_calls):
305
+ steps.append(Step(step_id=0, source="agent", message=answer))
306
+ for i, step in enumerate(steps, 1):
307
+ step.step_id = i
308
+ subagents = [
309
+ to_atif(
310
+ [n, *tree.subtree(n.span_id)],
311
+ mapping,
312
+ agent_name=n.agent_name or n.name,
313
+ session_id=session_id,
314
+ mapping_version=mapping_version,
315
+ )
316
+ for n in top_nested
317
+ ]
318
+ ordered_main = sorted(main, key=lambda x: x.started_at)
319
+ first_model = next((s.model for s in ordered_main if s.kind == "llm" and s.model), None)
320
+ extra: dict[str, Any] = {"trajectory_source": "otel", "mapping_version": mapping_version}
321
+ if incomplete:
322
+ extra["incomplete_spans"] = incomplete
323
+ unplaced_tools = [s.name for s in unplaced if s.kind == "tool"]
324
+ if unplaced_tools:
325
+ extra["unplaced_tool_spans"] = unplaced_tools
326
+ anchor = main_root or (kept[0] if kept else None)
327
+ return Trajectory(
328
+ schema_version=ATIF_VERSION,
329
+ session_id=session_id,
330
+ trajectory_id=f"{anchor.trace_id}:{anchor.span_id}" if anchor else None,
331
+ agent=AtifAgent(name=agent_name, model_name=first_model),
332
+ steps=steps,
333
+ final_metrics=_final_metrics(steps, subagents),
334
+ subagent_trajectories=subagents or None,
335
+ extra=extra,
336
+ )
@@ -0,0 +1,113 @@
1
+ from datetime import datetime
2
+ from typing import Protocol
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from evalkit.contracts.enums import TraceImportStatus
7
+ from evalkit.contracts.esf import MappingDocument
8
+ from evalkit.core.runner import error_text
9
+ from evalkit.core.traces.mapping import session_id_of
10
+ from evalkit.core.traces.raw import RawSpan
11
+ from evalkit.core.traces.sources import TraceSourceClient
12
+
13
+ IMPORT_LEASE_SECONDS = 180
14
+ IMPORT_STOPPED_ERROR = "the server stopped while importing"
15
+ ACTIVE_IMPORT = frozenset({TraceImportStatus.RUNNING, TraceImportStatus.STOPPING})
16
+
17
+
18
+ class ImportJob(BaseModel):
19
+ id: int
20
+ agent_id: int
21
+ trace_source_id: int
22
+ project: str
23
+ until: datetime
24
+ resume_from: datetime
25
+ resume_trace_ids: list[str]
26
+
27
+
28
+ class ImportStore(Protocol):
29
+ async def job(self, import_id: int) -> ImportJob: ...
30
+
31
+ async def advance(
32
+ self,
33
+ import_id: int,
34
+ holder: str,
35
+ *,
36
+ resume_from: datetime,
37
+ resume_trace_ids: list[str],
38
+ traces: int,
39
+ spans: int,
40
+ sample_session: str | None,
41
+ ) -> TraceImportStatus | None: ...
42
+
43
+ async def renew(self, import_id: int, holder: str) -> TraceImportStatus | None: ...
44
+
45
+ async def finish(
46
+ self, import_id: int, holder: str, status: TraceImportStatus, error: str | None = None
47
+ ) -> None: ...
48
+
49
+
50
+ class SpanWriter(Protocol):
51
+ async def upsert(
52
+ self, agent_id: int, spans: list[RawSpan], *, external_trace_source_id: int | None = None
53
+ ) -> int: ...
54
+
55
+
56
+ async def run_import(
57
+ import_id: int,
58
+ holder: str,
59
+ client: TraceSourceClient,
60
+ store: ImportStore,
61
+ spans: SpanWriter,
62
+ mapping: MappingDocument | None,
63
+ ) -> None:
64
+ job = await store.job(import_id)
65
+ boundary, written = job.resume_from, set(job.resume_trace_ids)
66
+ async for page in client.trace_pages(job.project, job.resume_from, job.until):
67
+ fresh = [tid for tid, start in page.traces if start != boundary or tid not in written]
68
+ span_count, sample = 0, None
69
+ if fresh:
70
+ async for batch in client.trace_spans(job.project, fresh):
71
+ for span in batch:
72
+ span.session_id_hint = session_id_of(span.attributes, mapping)
73
+ await spans.upsert(
74
+ job.agent_id, batch, external_trace_source_id=job.trace_source_id
75
+ )
76
+ span_count += len(batch)
77
+ roots = [s.session_id_hint for s in batch if s.parent_span_id is None]
78
+ sample = next((r for r in reversed(roots) if r), sample)
79
+ if await store.renew(import_id, holder) is None:
80
+ return
81
+ at_last = {tid for tid, start in page.traces if start == page.last_start}
82
+ written = written | at_last if page.last_start == boundary else at_last
83
+ boundary = page.last_start
84
+ state = await store.advance(
85
+ import_id,
86
+ holder,
87
+ resume_from=boundary,
88
+ resume_trace_ids=sorted(written),
89
+ traces=len(fresh),
90
+ spans=span_count,
91
+ sample_session=sample,
92
+ )
93
+ if state is None:
94
+ return
95
+ if state is TraceImportStatus.STOPPING:
96
+ await store.finish(import_id, holder, TraceImportStatus.STOPPED)
97
+ return
98
+ await store.finish(import_id, holder, TraceImportStatus.DONE)
99
+
100
+
101
+ async def guarded_import(
102
+ import_id: int,
103
+ holder: str,
104
+ client: TraceSourceClient,
105
+ store: ImportStore,
106
+ spans: SpanWriter,
107
+ mapping: MappingDocument | None,
108
+ ) -> None:
109
+ try:
110
+ await run_import(import_id, holder, client, store, spans, mapping)
111
+ except Exception as exc:
112
+ await store.finish(import_id, holder, TraceImportStatus.FAILED, error_text(exc))
113
+ raise