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,494 @@
1
+ """The answer-rubric evaluator — one judge call per case, seven named scores.
2
+
3
+ The metrics section carries the formula for each. Nothing here is a gauge: the
4
+ content score, its two factors, the three per-type rates and the citation rate
5
+ are all evaluations, so each lands in the evaluator matrix and can be pooled
6
+ across the corpus.
7
+
8
+ coverage = attempted / total did the answer address the rubric at all
9
+ accuracy = satisfied / attempted of what it addressed, how much is right
10
+ recall = satisfied / total identically coverage x accuracy
11
+
12
+ `accuracy` is not precision: its denominator does not count what the answer
13
+ volunteered that no rubric asked about, so a long answer is not marked down for
14
+ its length. Nothing here measures unrequested content at all.
15
+
16
+ `source` is graded on its own axis and never enters the content counts — with
17
+ every rubric counting once, a `source` would otherwise weigh as much as the fact
18
+ it hangs off, and a case where every fact carries one would put half its content
19
+ score on citations.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import logging
26
+ from dataclasses import dataclass
27
+ from typing import Any, Literal
28
+
29
+ from pydantic import BaseModel, Field
30
+
31
+ from evalkit.contracts.cases import AnswerRubrics, AnswerRubricType, Rubric
32
+ from evalkit.core.evaluators.base import EvalContext, builtin_evaluator, label_from_score
33
+ from evalkit.core.fixtures import compose_input
34
+ from evalkit.core.judge import call_judge
35
+ from evalkit.store.base import ScoreRow
36
+
37
+ LOGGER = logging.getLogger(__name__)
38
+
39
+ RUBRIC_TYPES: tuple[AnswerRubricType, ...] = (
40
+ "factual_retrieval",
41
+ "derived_computation",
42
+ "analysis_interpretation",
43
+ )
44
+
45
+ SCORE_NAME = "answer_rubric_score"
46
+ COVERAGE_NAME = "answer_rubric_coverage"
47
+ ACCURACY_NAME = "answer_rubric_accuracy"
48
+ CITATION_NAME = "answer_rubric_citation"
49
+
50
+
51
+ def type_score_name(rubric_type: AnswerRubricType) -> str:
52
+ return f"answer_rubric_{rubric_type}"
53
+
54
+
55
+ ANSWER_RUBRIC_EMITS: tuple[str, ...] = (
56
+ SCORE_NAME,
57
+ COVERAGE_NAME,
58
+ ACCURACY_NAME,
59
+ *(type_score_name(t) for t in RUBRIC_TYPES),
60
+ CITATION_NAME,
61
+ )
62
+
63
+ RubricVerdictLabel = Literal["satisfied", "incorrect", "absent"]
64
+
65
+ VERDICTS: tuple[RubricVerdictLabel, ...] = ("satisfied", "incorrect", "absent")
66
+
67
+
68
+ class SourceVerdict(BaseModel):
69
+ verdict: RubricVerdictLabel = Field(
70
+ description="""\
71
+ satisfied: the answer attributes this fact to that source.
72
+ incorrect: it attributes the fact to a DIFFERENT source.
73
+ absent: it states the fact with no attribution at all."""
74
+ )
75
+ stated: str = Field(
76
+ default="",
77
+ description="The attribution the answer gave, quoted verbatim. Empty when absent.",
78
+ )
79
+ note: str = Field(
80
+ default="",
81
+ description="One line on why this verdict, when not self-evident.",
82
+ )
83
+
84
+
85
+ class RubricVerdict(BaseModel):
86
+ index: int = Field(
87
+ description="The number this rubric was listed under in the prompt, counting from 1."
88
+ )
89
+ verdict: RubricVerdictLabel = Field(
90
+ description="""\
91
+ satisfied: the answer meets the rubric.
92
+ incorrect: it attaches a value or assertion to the rubric's subject and that
93
+ does not meet the rubric.
94
+ absent: it attaches no value or assertion to the rubric's subject."""
95
+ )
96
+ stated: str = Field(
97
+ default="",
98
+ description="What the answer said on this point, quoted verbatim. Empty when absent.",
99
+ )
100
+ note: str = Field(
101
+ default="",
102
+ description="One line on why this verdict. Required for incorrect and absent:"
103
+ " what the answer said or left out, and how that misses the rubric.",
104
+ )
105
+ # No default, so `source` stays in the schema's `required` list. Optional it
106
+ # renders as `anyOf: [$ref, null]` outside `required`, and Gemini never emits
107
+ # the field at all.
108
+ source: SourceVerdict | None = Field(
109
+ description="A verdict object for a rubric shown with a `source:` line;"
110
+ " null for a rubric shown without one.",
111
+ )
112
+
113
+
114
+ class RubricJudgeOutput(BaseModel):
115
+ verdicts: list[RubricVerdict]
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class GradedRubric:
120
+ """One rubric with the verdict it received.
121
+
122
+ `source_verdict` is None where the case declared no `source` — the
123
+ judge-gap check guarantees that a rubric shown with one comes back with a
124
+ sub-verdict, so None here means "no source asked for", never "not graded".
125
+ """
126
+
127
+ type: AnswerRubricType
128
+ verdict: RubricVerdictLabel
129
+ source_verdict: RubricVerdictLabel | None = None
130
+
131
+
132
+ class RubricMetrics(BaseModel):
133
+ coverage: float | None = None
134
+ accuracy: float | None = None
135
+ recall: float | None = None
136
+
137
+ satisfied: int = 0
138
+ incorrect: int = 0
139
+ absent: int = 0
140
+ total: int = 0
141
+ attempted: int = 0
142
+
143
+
144
+ class CitationMetrics(BaseModel):
145
+ coverage: float | None = None
146
+ accuracy: float | None = None
147
+ rate: float | None = None
148
+
149
+ satisfied: int = 0
150
+ incorrect: int = 0
151
+ absent: int = 0
152
+ eligible: int = 0
153
+ attempted: int = 0
154
+
155
+
156
+ def _counts(verdicts: list[RubricVerdictLabel]) -> dict[str, int]:
157
+ counted: dict[str, int] = {verdict: 0 for verdict in VERDICTS}
158
+ for verdict in verdicts:
159
+ if verdict not in counted:
160
+ raise ValueError(f"unknown verdict {verdict!r}")
161
+ counted[verdict] += 1
162
+ return counted
163
+
164
+
165
+ def aggregate(rubrics: list[GradedRubric]) -> RubricMetrics:
166
+ n = _counts([r.verdict for r in rubrics])
167
+ total = sum(n.values())
168
+ attempted = n["satisfied"] + n["incorrect"]
169
+ return RubricMetrics(
170
+ # None, never 0.0: bucket aggregation drops None but averages 0.0 in, so
171
+ # an unmeasured metric would otherwise drag down every aggregate it
172
+ # touches.
173
+ coverage=attempted / total if total else None,
174
+ accuracy=n["satisfied"] / attempted if attempted else None,
175
+ recall=n["satisfied"] / total if total else None,
176
+ satisfied=n["satisfied"],
177
+ incorrect=n["incorrect"],
178
+ absent=n["absent"],
179
+ total=total,
180
+ attempted=attempted,
181
+ )
182
+
183
+
184
+ def by_type(rubrics: list[GradedRubric]) -> dict[AnswerRubricType, RubricMetrics]:
185
+ """The same count over each type's subset.
186
+
187
+ A type the case carries no rubrics of yields no entry, so its evaluator
188
+ skips rather than scoring zero.
189
+ """
190
+ present = {r.type for r in rubrics}
191
+ return {t: aggregate([r for r in rubrics if r.type == t]) for t in present}
192
+
193
+
194
+ def citation_rate(rubrics: list[GradedRubric]) -> CitationMetrics:
195
+ """The citation axis, over the sources the answer had a chance to give.
196
+
197
+ A `source` counts only where the answer actually stated the parent fact. An
198
+ answer that never mentioned the fact had no chance to cite it, and counting
199
+ that as a citation miss would make this rate a shadow of the content rate
200
+ instead of saying something new.
201
+ """
202
+ eligible = [
203
+ r.source_verdict for r in rubrics if r.source_verdict is not None and r.verdict != "absent"
204
+ ]
205
+ n = _counts(eligible)
206
+ count = sum(n.values())
207
+ attempted = n["satisfied"] + n["incorrect"]
208
+ return CitationMetrics(
209
+ coverage=attempted / count if count else None,
210
+ accuracy=n["satisfied"] / attempted if attempted else None,
211
+ rate=n["satisfied"] / count if count else None,
212
+ satisfied=n["satisfied"],
213
+ incorrect=n["incorrect"],
214
+ absent=n["absent"],
215
+ eligible=count,
216
+ attempted=attempted,
217
+ )
218
+
219
+
220
+ def missing_verdict_indices(expected_count: int, indices: list[int]) -> list[int]:
221
+ """Rubric indices (1-based) the judge did not verdict exactly once."""
222
+ seen: dict[int, int] = {}
223
+ for index in indices:
224
+ seen[index] = seen.get(index, 0) + 1
225
+ return [i for i in range(1, expected_count + 1) if seen.get(i, 0) != 1]
226
+
227
+
228
+ def missing_source_indices(
229
+ sourced_indices: list[int], graded_source_indices: set[int]
230
+ ) -> list[int]:
231
+ """Indices shown with a `source:` line that came back with no source verdict."""
232
+ return [i for i in sourced_indices if i not in graded_source_indices]
233
+
234
+
235
+ PREAMBLE = """\
236
+ You are an experienced financial analyst grading another analyst's work. You are
237
+ given the question that was asked, the answer, and a numbered list of rubrics,
238
+ and you return a verdict for each. You are strict but fair: a rubric is
239
+ satisfied only on positive evidence."""
240
+
241
+ _VERDICT_LEGEND = """\
242
+ Each line below is one rubric to verdict, formatted `<n>. <assertion>`.
243
+
244
+ For each rubric return one verdict:
245
+ satisfied the answer meets the rubric
246
+ incorrect the answer attaches a value or assertion to the rubric's subject,
247
+ and it does not meet the rubric
248
+ absent the answer attaches no value or assertion to the rubric's subject
249
+
250
+ A rubric shown with a `source:` line also gets a verdict on that line: whether
251
+ the answer attributes this fact to that source. Equivalent naming counts — "the
252
+ Q3 FY2024 results", "the Q3 press release" and "the company's fiscal Q3 2024
253
+ results" are the same source. Attributing the fact to a DIFFERENT document is
254
+ `incorrect`; stating the fact with no attribution at all is `absent`. The
255
+ attribution may sit anywhere in the answer — beside the fact, in the paragraph
256
+ introducing it, or in a references section at the end — as long as it is clear
257
+ that it covers this fact.
258
+
259
+ Grade each `source:` line independently of the rubric above it, and grade every
260
+ rubric independently of the others. Silence is not satisfaction; absence of
261
+ contradiction is not satisfaction.
262
+
263
+ Every `incorrect` or `absent` verdict carries a note saying what the answer said
264
+ or left out and how that misses the rubric."""
265
+
266
+ _MATCHING_LEGEND = """\
267
+ NUMERIC MATCHING
268
+ A figure in the answer satisfies a rubric when it matches the rubric's figure
269
+ after unit conversion, accepting normal rounding: an answer figure that is a
270
+ rounding of the rubric's figure satisfies it. Against "3,172.6 million",
271
+ "3,173 million" and "3.17 billion" are satisfied; "3,180 million" is a
272
+ different figure, not a rounding, so it is incorrect.
273
+
274
+ Where the rubric states its own acceptance criterion ("±1 million", "any
275
+ figure between X and Y", "either the standalone or the consolidated figure"),
276
+ that criterion replaces this default — accept exactly what it accepts.
277
+
278
+ Equality is on the numeric value, not the string: "$3,172.6 million",
279
+ "3172.60 million" and "$3.1726 billion" are the same figure."""
280
+
281
+ _UNIT_LEGEND = """\
282
+ UNITS
283
+ Each rubric states its unit. Convert the answer's figure into that unit before
284
+ comparing; never compare raw figures.
285
+ scale: lakh = 1e5 · crore = 1e7 · million = 1e6 · billion = 1e9
286
+ rate: 1 percent = 100 bps
287
+ sign: a figure in parentheses inside a financial statement, table or
288
+ calculation is negative — "(1,234)" is -1234. Parentheses in prose are
289
+ not a sign: "revenue rose 12% (driven by growth)" carries no negative.
290
+ Direction words — outflow, loss, decline, decrease — also mean negative."""
291
+
292
+
293
+ def render_rubric(index: int, rubric: Rubric) -> str:
294
+ line = f"{index:>2}. {rubric.rubric}"
295
+ if rubric.source is not None:
296
+ line += f"\n source: {rubric.source}"
297
+ return line
298
+
299
+
300
+ def user_prompt(question: str, rubrics: list[Rubric], answer: str) -> str:
301
+ lines = [render_rubric(i, rubric) for i, rubric in enumerate(rubrics, start=1)]
302
+ return "\n".join(
303
+ [
304
+ _VERDICT_LEGEND,
305
+ "",
306
+ _MATCHING_LEGEND,
307
+ "",
308
+ _UNIT_LEGEND,
309
+ "",
310
+ "QUESTION",
311
+ question,
312
+ "",
313
+ "RUBRICS",
314
+ "\n\n".join(lines),
315
+ "",
316
+ "ANSWER",
317
+ answer,
318
+ ]
319
+ )
320
+
321
+
322
+ class IncompleteVerdicts(Exception):
323
+ """The judge did not verdict every rubric, and every source, exactly once,
324
+ with a note on every verdict that is not satisfied."""
325
+
326
+
327
+ def unexplained_unsatisfied_indices(verdicts: list[RubricVerdict]) -> list[int]:
328
+ return [v.index for v in verdicts if v.verdict != "satisfied" and not v.note.strip()]
329
+
330
+
331
+ def _gaps(rubrics: list[Rubric], parsed: RubricJudgeOutput) -> str | None:
332
+ """What the judge left ungraded, as a phrase for the retry, or None."""
333
+ missing = missing_verdict_indices(len(rubrics), [v.index for v in parsed.verdicts])
334
+ sourced = [i for i, r in enumerate(rubrics, start=1) if r.source is not None]
335
+ graded_sources = {v.index for v in parsed.verdicts if v.source is not None}
336
+ missing_sources = missing_source_indices(sourced, graded_sources)
337
+ unexplained = unexplained_unsatisfied_indices(parsed.verdicts)
338
+ parts = []
339
+ if missing:
340
+ parts.append(f"exactly one verdict for rubrics {missing}")
341
+ if missing_sources:
342
+ parts.append(f"a source verdict for rubrics {missing_sources}")
343
+ if unexplained:
344
+ parts.append(f"a note on non-satisfied rubrics {unexplained}")
345
+ if not parts:
346
+ return None
347
+ if len(parts) == 1:
348
+ return parts[0]
349
+ return ", ".join(parts[:-1]) + f" and {parts[-1]}"
350
+
351
+
352
+ async def _judge_verdicts(
353
+ ctx: EvalContext, rubrics: list[Rubric], base_prompt: str
354
+ ) -> RubricJudgeOutput:
355
+ """One judge call, retried once when the verdict list has a gap.
356
+
357
+ A gap is not scored as the agent's failure — an incomplete response is a
358
+ judge lapse, and defaulting the missing rubrics to `absent` would blame the
359
+ agent for it.
360
+ """
361
+ current = base_prompt
362
+ gaps: str | None = None
363
+ for attempt in (1, 2):
364
+ parsed, usage = await call_judge(
365
+ ctx.judge_cfg,
366
+ api_key=ctx.judge_api_key,
367
+ system_prompt=PREAMBLE,
368
+ user_prompt=current,
369
+ output_type=RubricJudgeOutput,
370
+ session_id=ctx.session_id,
371
+ evaluator_name="answer_rubrics",
372
+ )
373
+ ctx.record_judge_usage(usage)
374
+ gaps = _gaps(rubrics, parsed)
375
+ if gaps is None:
376
+ return parsed
377
+ LOGGER.warning("evals.answer_rubrics.incomplete_verdicts attempt=%s gaps=%s", attempt, gaps)
378
+ current = (
379
+ f"{base_prompt}\n\nYour previous response did not return {gaps}. Return "
380
+ f"one verdict per rubric, for every rubric from 1 to {len(rubrics)}, a "
381
+ "source verdict for every rubric shown with a `source:` line, and a note on "
382
+ "every verdict that is not satisfied."
383
+ )
384
+ raise IncompleteVerdicts(f"judge left {gaps} after a retry")
385
+
386
+
387
+ def _grade(rubrics: list[Rubric], verdicts: list[RubricVerdict]) -> list[GradedRubric]:
388
+ by_index = {v.index: v for v in verdicts}
389
+ graded = []
390
+ for index, rubric in enumerate(rubrics, start=1):
391
+ verdict = by_index[index]
392
+ graded.append(
393
+ GradedRubric(
394
+ type=rubric.type,
395
+ verdict=verdict.verdict,
396
+ source_verdict=(
397
+ verdict.source.verdict
398
+ if rubric.source is not None and verdict.source is not None
399
+ else None
400
+ ),
401
+ )
402
+ )
403
+ return graded
404
+
405
+
406
+ def _explanation(
407
+ content: RubricMetrics,
408
+ per_type: dict[AnswerRubricType, RubricMetrics],
409
+ citation: CitationMetrics,
410
+ rubrics: list[Rubric],
411
+ verdicts: list[RubricVerdict],
412
+ ) -> str:
413
+ """The only structured field a score row carries, so the counts and
414
+ verdicts ride in it as JSON. The scorecard's pooled rates and its BY TYPE /
415
+ CITATION tables are all read back out of here. Each verdict carries its
416
+ rubric's text and type so the table is readable without the case config."""
417
+ by_index = {index: rubric for index, rubric in enumerate(rubrics, start=1)}
418
+ rows = []
419
+ for verdict in verdicts:
420
+ rubric = by_index.get(verdict.index)
421
+ rows.append(
422
+ {
423
+ **verdict.model_dump(),
424
+ "rubric": rubric.rubric if rubric else None,
425
+ "type": rubric.type if rubric else None,
426
+ }
427
+ )
428
+ return json.dumps(
429
+ {
430
+ **content.model_dump(),
431
+ "by_type": {t: m.model_dump() for t, m in per_type.items()},
432
+ "citation": citation.model_dump(),
433
+ "verdicts": rows,
434
+ },
435
+ ensure_ascii=False,
436
+ )
437
+
438
+
439
+ def _row(name: str, score: float | None, explanation: str, skip_reason: str) -> ScoreRow:
440
+ if score is None:
441
+ return ScoreRow(name=name, score=None, label="skip", explanation=skip_reason)
442
+ return ScoreRow(
443
+ name=name,
444
+ score=score,
445
+ label=label_from_score(score),
446
+ explanation=explanation,
447
+ annotator_kind="LLM",
448
+ )
449
+
450
+
451
+ @builtin_evaluator("answer_rubrics", emits=ANSWER_RUBRIC_EMITS, uses_judge=True)
452
+ async def answer_rubrics(ctx: EvalContext, config: dict[str, Any]) -> list[ScoreRow]:
453
+ cfg = AnswerRubrics.model_validate(config)
454
+ question = str(compose_input(ctx.case.input, ctx.case.files).get("question", ""))
455
+ parsed = await _judge_verdicts(ctx, cfg.rubrics, user_prompt(question, cfg.rubrics, ctx.answer))
456
+
457
+ graded = _grade(cfg.rubrics, parsed.verdicts)
458
+ content = aggregate(graded)
459
+ per_type = by_type(graded)
460
+ citation = citation_rate(graded)
461
+
462
+ # Every scored name carries the SAME explanation JSON. The scorecard pools
463
+ # each name from its own row, so each one has to be able to find its counts
464
+ # there; and a reader opening any cell in the matrix gets the whole verdict
465
+ # list rather than only the slice that name scored.
466
+ explanation = _explanation(content, per_type, citation, cfg.rubrics, parsed.verdicts)
467
+
468
+ rows = [
469
+ _row(SCORE_NAME, content.recall, explanation, "no rubrics — nothing to score"),
470
+ _row(COVERAGE_NAME, content.coverage, explanation, "no rubrics — nothing to engage"),
471
+ _row(
472
+ ACCURACY_NAME,
473
+ content.accuracy,
474
+ explanation,
475
+ "no rubric was engaged — accuracy undefined",
476
+ ),
477
+ _row(
478
+ CITATION_NAME,
479
+ citation.rate,
480
+ explanation,
481
+ "no source was eligible — the answer stated none of the sourced facts",
482
+ ),
483
+ ]
484
+ for rubric_type in RUBRIC_TYPES:
485
+ metrics = per_type.get(rubric_type)
486
+ rows.append(
487
+ _row(
488
+ type_score_name(rubric_type),
489
+ metrics.recall if metrics else None,
490
+ explanation,
491
+ f"the case carries no {rubric_type} rubrics",
492
+ )
493
+ )
494
+ return rows
@@ -0,0 +1,187 @@
1
+ """Deterministic checks over the tool calls in the recorded trajectory.
2
+
3
+ tool_trajectory_match: score = matched / len(expected)
4
+ unordered: matched = size of a maximum one-to-one matching between
5
+ expected entries and actual calls
6
+ ordered: matched = longest common subsequence of (expected, actual)
7
+
8
+ required_tools_called: score = |required ∩ called| / |required|
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from dataclasses import dataclass
15
+ from typing import Any
16
+
17
+ from evalkit.contracts.atif import ToolCall
18
+ from evalkit.contracts.cases import ExpectedToolCall, RequiredToolsCalled, ToolTrajectoryMatch
19
+ from evalkit.core.evaluators.base import EvalContext, builtin_evaluator, neutral_skip
20
+ from evalkit.store.base import ScoreRow
21
+
22
+ REQUIRES = {"trajectory", "tool_calls"}
23
+
24
+
25
+ @dataclass
26
+ class EntryDiagnostic:
27
+ expected: ExpectedToolCall
28
+ matched: bool
29
+ reason: str = ""
30
+
31
+
32
+ @dataclass
33
+ class TrajectoryMatchResult:
34
+ satisfied: int
35
+ total: int
36
+ diagnostics: list[EntryDiagnostic]
37
+
38
+
39
+ def _searchable_text(value: Any) -> str:
40
+ text = value if isinstance(value, str) else json.dumps(value, default=str)
41
+ return text.lower()
42
+
43
+
44
+ def _arg_value_matches(mode: str, actual_value: Any, expected_value: Any) -> bool:
45
+ if mode == "substring":
46
+ return str(expected_value).lower() in _searchable_text(actual_value)
47
+ return bool(actual_value == expected_value)
48
+
49
+
50
+ def tool_call_matches(expected: ExpectedToolCall, actual: ToolCall) -> bool:
51
+ if expected.name != actual.function_name:
52
+ return False
53
+ if expected.args_mode == "ignore":
54
+ return True
55
+ if expected.args_mode == "exact":
56
+ return actual.arguments == expected.args
57
+ default_mode = "substring" if expected.args_mode == "substring" else "equals"
58
+ for key, value in expected.args.items():
59
+ if key not in actual.arguments:
60
+ return False
61
+ mode = expected.match_overrides.get(key, default_mode)
62
+ if not _arg_value_matches(mode, actual.arguments[key], value):
63
+ return False
64
+ return True
65
+
66
+
67
+ def _diagnose(expected: ExpectedToolCall, actual: list[ToolCall], *, ordered: bool) -> str:
68
+ if not any(a.function_name == expected.name for a in actual):
69
+ return "name miss"
70
+ if not any(tool_call_matches(expected, a) for a in actual):
71
+ return "args mismatch"
72
+ return "out of order" if ordered else "no available matching call"
73
+
74
+
75
+ def _unordered_matched(expected: list[ExpectedToolCall], actual: list[ToolCall]) -> set[int]:
76
+ adj = [[j for j, a in enumerate(actual) if tool_call_matches(e, a)] for e in expected]
77
+ match_right: dict[int, int] = {}
78
+
79
+ def augment(i: int, visited: set[int]) -> bool:
80
+ for j in adj[i]:
81
+ if j in visited:
82
+ continue
83
+ visited.add(j)
84
+ if j not in match_right or augment(match_right[j], visited):
85
+ match_right[j] = i
86
+ return True
87
+ return False
88
+
89
+ for i in range(len(expected)):
90
+ augment(i, set())
91
+ return set(match_right.values())
92
+
93
+
94
+ def _ordered_matched(expected: list[ExpectedToolCall], actual: list[ToolCall]) -> set[int]:
95
+ m, n = len(expected), len(actual)
96
+ hit = [[tool_call_matches(e, a) for a in actual] for e in expected]
97
+ dp = [[0] * (n + 1) for _ in range(m + 1)]
98
+ for i in range(1, m + 1):
99
+ for j in range(1, n + 1):
100
+ if hit[i - 1][j - 1]:
101
+ dp[i][j] = dp[i - 1][j - 1] + 1
102
+ else:
103
+ dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
104
+ matched: set[int] = set()
105
+ i, j = m, n
106
+ while i > 0 and j > 0:
107
+ if hit[i - 1][j - 1] and dp[i][j] == dp[i - 1][j - 1] + 1:
108
+ matched.add(i - 1)
109
+ i -= 1
110
+ j -= 1
111
+ elif dp[i - 1][j] >= dp[i][j - 1]:
112
+ i -= 1
113
+ else:
114
+ j -= 1
115
+ return matched
116
+
117
+
118
+ def match_trajectory(
119
+ expected: list[ExpectedToolCall], actual: list[ToolCall], *, ordered: bool
120
+ ) -> TrajectoryMatchResult:
121
+ matched = (
122
+ _ordered_matched(expected, actual) if ordered else _unordered_matched(expected, actual)
123
+ )
124
+ diagnostics = [
125
+ EntryDiagnostic(
126
+ expected=e,
127
+ matched=i in matched,
128
+ reason="" if i in matched else _diagnose(e, actual, ordered=ordered),
129
+ )
130
+ for i, e in enumerate(expected)
131
+ ]
132
+ return TrajectoryMatchResult(
133
+ satisfied=len(matched), total=len(expected), diagnostics=diagnostics
134
+ )
135
+
136
+
137
+ @builtin_evaluator("tool_trajectory_match", requires=REQUIRES)
138
+ async def tool_trajectory_match(ctx: EvalContext, config: dict[str, Any]) -> list[ScoreRow]:
139
+ cfg = ToolTrajectoryMatch.model_validate(config)
140
+ if ctx.trajectory_view is None:
141
+ return [neutral_skip("tool_trajectory_match", "no trajectory recorded for this run")]
142
+ result = match_trajectory(cfg.expected, ctx.trajectory_view.tool_calls(), ordered=cfg.ordered)
143
+ verdicts = [
144
+ {
145
+ "index": index,
146
+ "expected": d.expected.name,
147
+ "args": d.expected.args,
148
+ "args_mode": d.expected.args_mode,
149
+ "verdict": "satisfied" if d.matched else "unsatisfied",
150
+ "note": d.reason,
151
+ }
152
+ for index, d in enumerate(result.diagnostics, 1)
153
+ ]
154
+ explanation = {
155
+ "satisfied": result.satisfied,
156
+ "total": result.total,
157
+ "ordered": cfg.ordered,
158
+ "verdicts": verdicts,
159
+ }
160
+ return [
161
+ ScoreRow(
162
+ name="tool_trajectory_match",
163
+ score=result.satisfied / result.total,
164
+ explanation=json.dumps(explanation, ensure_ascii=False, default=str),
165
+ )
166
+ ]
167
+
168
+
169
+ @builtin_evaluator("required_tools_called", requires=REQUIRES)
170
+ async def required_tools_called(ctx: EvalContext, config: dict[str, Any]) -> list[ScoreRow]:
171
+ cfg = RequiredToolsCalled.model_validate(config)
172
+ if ctx.trajectory_view is None:
173
+ return [neutral_skip("required_tools_called", "no trajectory recorded for this run")]
174
+ required = list(dict.fromkeys(cfg.tools))
175
+ called = {c.function_name for c in ctx.trajectory_view.tool_calls()}
176
+ verdicts = [
177
+ {"tool": tool, "verdict": "satisfied" if tool in called else "missing"} for tool in required
178
+ ]
179
+ invoked = sum(1 for tool in required if tool in called)
180
+ explanation = {"invoked": invoked, "total": len(required), "verdicts": verdicts}
181
+ return [
182
+ ScoreRow(
183
+ name="required_tools_called",
184
+ score=invoked / len(required),
185
+ explanation=json.dumps(explanation, ensure_ascii=False),
186
+ )
187
+ ]