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.
- evalkit/__init__.py +0 -0
- evalkit/api/__init__.py +0 -0
- evalkit/api/access.py +74 -0
- evalkit/api/agents.py +159 -0
- evalkit/api/app.py +360 -0
- evalkit/api/auth.py +225 -0
- evalkit/api/cli_dist.py +10 -0
- evalkit/api/datasets.py +169 -0
- evalkit/api/deps.py +79 -0
- evalkit/api/download.py +15 -0
- evalkit/api/evaluators.py +121 -0
- evalkit/api/experiments.py +616 -0
- evalkit/api/fixtures.py +95 -0
- evalkit/api/harness_check.py +48 -0
- evalkit/api/imports.py +180 -0
- evalkit/api/limits.py +84 -0
- evalkit/api/llm_providers.py +120 -0
- evalkit/api/mcp/__init__.py +0 -0
- evalkit/api/mcp/auth.py +100 -0
- evalkit/api/mcp/descriptions.py +336 -0
- evalkit/api/mcp/models.py +259 -0
- evalkit/api/mcp/server.py +101 -0
- evalkit/api/mcp/tools.py +844 -0
- evalkit/api/meta.py +19 -0
- evalkit/api/otel.py +350 -0
- evalkit/api/pull.py +197 -0
- evalkit/api/review.py +271 -0
- evalkit/api/runs.py +555 -0
- evalkit/api/server_keys.py +44 -0
- evalkit/api/tokens.py +51 -0
- evalkit/api/trace_imports.py +173 -0
- evalkit/api/trace_sources.py +49 -0
- evalkit/api/traces.py +196 -0
- evalkit/api/users.py +72 -0
- evalkit/api/workspaces.py +131 -0
- evalkit/bundled/contracts/atif-accepted.json +5 -0
- evalkit/bundled/contracts/custom-evaluator-api-v1.json +243 -0
- evalkit/bundled/contracts/harness-api-v1.json +938 -0
- evalkit/bundled/contracts/span-mapping-v1.json +314 -0
- evalkit/bundled/docs/concepts.md +26 -0
- evalkit/bundled/docs/config.md +38 -0
- evalkit/bundled/docs/harness-api.md +44 -0
- evalkit/bundled/docs/install.md +60 -0
- evalkit/bundled/docs/login.md +36 -0
- evalkit/bundled/docs/mappings.md +136 -0
- evalkit/bundled/docs/mcp.md +44 -0
- evalkit/bundled/docs/quickstart.md +142 -0
- evalkit/bundled/docs/sdk-runs.md +171 -0
- evalkit/bundled/docs/tracing.md +171 -0
- evalkit/bundled/docs/try-locally.md +288 -0
- evalkit/bundled/docs/workspaces.md +45 -0
- evalkit/bundled/skills/authoring-eval-cases/SKILL.md +387 -0
- evalkit/bundled/skills/authoring-eval-cases/references/overlays.md +83 -0
- evalkit/bundled/skills/authoring-eval-cases/references/tool-checks.md +71 -0
- evalkit/bundled/skills/connect-traces/SKILL.md +296 -0
- evalkit/bundled/skills/connect-traces/references/gen_ai.json +42 -0
- evalkit/bundled/skills/connect-traces/references/openinference.json +35 -0
- evalkit/bundled/skills/connect-traces/references/span-mapping-schema.md +88 -0
- evalkit/bundled/skills/eval-driven-development/SKILL.md +458 -0
- evalkit/bundled/skills/eval-driven-development/references/analyze-run.md +97 -0
- evalkit/bundled/skills/eval-driven-development/references/diagnose.md +130 -0
- evalkit/bundled/skills/eval-driven-development/references/synthesize-cases.md +87 -0
- evalkit/bundled/skills/eval-driven-development/references/synthesize.md +165 -0
- evalkit/bundled/skills/evalkit-deployment/SKILL.md +332 -0
- evalkit/bundled/skills/evalkit-deployment/config.example.yaml +49 -0
- evalkit/bundled/skills/evalkit-deployment/docker-compose.yml +51 -0
- evalkit/bundled/skills/evalkit-deployment/references/config-and-deploy.md +333 -0
- evalkit/bundled/skills/evalkit-deployment/references/harness-api.md +139 -0
- evalkit/bundled/skills/evalkit-deployment/try/.env.example +11 -0
- evalkit/bundled/skills/evalkit-deployment/try/agent.py +131 -0
- evalkit/bundled/skills/evalkit-deployment/try/cases/acme/balance-sheet/cases.yaml +25 -0
- evalkit/bundled/skills/evalkit-deployment/try/cases/acme/income-statement/cases.yaml +43 -0
- evalkit/bundled/skills/evalkit-deployment/try/config.yaml +17 -0
- evalkit/bundled/skills/evalkit-deployment/try/docker-compose.yml +82 -0
- evalkit/bundled/skills/evalkit-deployment/try/evalkit.yaml +4 -0
- evalkit/bundled/skills/evalkit-deployment/try/mapping.json +42 -0
- evalkit/bundled/skills/evalkit-deployment/try/run.py +33 -0
- evalkit/bundled/skills/evalkit-usage/SKILL.md +509 -0
- evalkit/bundled/skills/evalkit-usage/references/suite-fixtures.md +42 -0
- evalkit/bundled/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- evalkit/bundled/ui/assets/abap-08VXUWAP.js +1 -0
- evalkit/bundled/ui/assets/apex-BWPQTe0t.js +1 -0
- evalkit/bundled/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- evalkit/bundled/ui/assets/bat-i0X4ZdIN.js +1 -0
- evalkit/bundled/ui/assets/bicep-B5-_aFwp.js +2 -0
- evalkit/bundled/ui/assets/cameligo-DMUM7wLl.js +1 -0
- evalkit/bundled/ui/assets/clojure-Cm7r79vr.js +1 -0
- evalkit/bundled/ui/assets/codicon-DF1abBS2.ttf +0 -0
- evalkit/bundled/ui/assets/coffee-Ba7i2nA0.js +1 -0
- evalkit/bundled/ui/assets/cpp-C7h46wYY.js +1 -0
- evalkit/bundled/ui/assets/csharp-BKxtCVv1.js +1 -0
- evalkit/bundled/ui/assets/csp-bTuwJoIa.js +1 -0
- evalkit/bundled/ui/assets/css-DIMkf-bt.js +3 -0
- evalkit/bundled/ui/assets/css.worker-DwfUZ3fB.js +96 -0
- evalkit/bundled/ui/assets/cssMode-C97l1pbB.js +1 -0
- evalkit/bundled/ui/assets/cypher-CVaqCwHa.js +1 -0
- evalkit/bundled/ui/assets/dart-onAF5SnQ.js +1 -0
- evalkit/bundled/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- evalkit/bundled/ui/assets/ecl-94qehhXe.js +1 -0
- evalkit/bundled/ui/assets/editor-DjpsE8to.css +1 -0
- evalkit/bundled/ui/assets/editor.api-NTesIxw5.js +908 -0
- evalkit/bundled/ui/assets/editor.worker-CRz__AKW.js +33 -0
- evalkit/bundled/ui/assets/elixir-6RTg0lbw.js +1 -0
- evalkit/bundled/ui/assets/flow9-C5_-GSwl.js +1 -0
- evalkit/bundled/ui/assets/freemarker2-BWqEFagp.js +3 -0
- evalkit/bundled/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- evalkit/bundled/ui/assets/go-C-y9NEjX.js +1 -0
- evalkit/bundled/ui/assets/graphql-fmXr3nnJ.js +1 -0
- evalkit/bundled/ui/assets/handlebars-DkIF-whN.js +1 -0
- evalkit/bundled/ui/assets/hcl-CpzslTdj.js +1 -0
- evalkit/bundled/ui/assets/html-C_rpeQXJ.js +1 -0
- evalkit/bundled/ui/assets/html.worker-D9ugpX4y.js +509 -0
- evalkit/bundled/ui/assets/htmlMode-B0YIq3DF.js +1 -0
- evalkit/bundled/ui/assets/index-Cw-jBH6M.js +736 -0
- evalkit/bundled/ui/assets/index-DKtoQZtm.css +1 -0
- evalkit/bundled/ui/assets/ini-sBoK_t0W.js +1 -0
- evalkit/bundled/ui/assets/java-BEtHBSE6.js +1 -0
- evalkit/bundled/ui/assets/javascript-D4EGhYA6.js +1 -0
- evalkit/bundled/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- evalkit/bundled/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- evalkit/bundled/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- evalkit/bundled/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- evalkit/bundled/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- evalkit/bundled/ui/assets/json.worker-CV9fqkUp.js +65 -0
- evalkit/bundled/ui/assets/jsonMode-pQjSfIH6.js +7 -0
- evalkit/bundled/ui/assets/julia-Bri6UV-V.js +1 -0
- evalkit/bundled/ui/assets/kotlin-BOotOW0E.js +1 -0
- evalkit/bundled/ui/assets/less-B9JPFI3C.js +2 -0
- evalkit/bundled/ui/assets/lexon-CfSJPG6W.js +1 -0
- evalkit/bundled/ui/assets/liquid-DbrvoYjD.js +1 -0
- evalkit/bundled/ui/assets/lspLanguageFeatures-D0B7cH6y.js +4 -0
- evalkit/bundled/ui/assets/lua-CsQS60Ue.js +1 -0
- evalkit/bundled/ui/assets/m3-D-oSqn_W.js +1 -0
- evalkit/bundled/ui/assets/markdown-Cimd5fb3.js +1 -0
- evalkit/bundled/ui/assets/mdx-B0HrujLR.js +1 -0
- evalkit/bundled/ui/assets/mips-CIPQ_RoX.js +1 -0
- evalkit/bundled/ui/assets/monacoEditor-DsoPncxW.js +68 -0
- evalkit/bundled/ui/assets/monacoEditor-n7clI0si.css +1 -0
- evalkit/bundled/ui/assets/msdax-DauUninz.js +1 -0
- evalkit/bundled/ui/assets/mysql-SOo6toE5.js +1 -0
- evalkit/bundled/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- evalkit/bundled/ui/assets/pascal-DrH0SRf2.js +1 -0
- evalkit/bundled/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- evalkit/bundled/ui/assets/perl-oz_6vUea.js +1 -0
- evalkit/bundled/ui/assets/pgsql-DTj74zXo.js +1 -0
- evalkit/bundled/ui/assets/php-nr791fC2.js +1 -0
- evalkit/bundled/ui/assets/pla-CopQ2nXW.js +1 -0
- evalkit/bundled/ui/assets/postiats-43DmfD33.js +1 -0
- evalkit/bundled/ui/assets/powerquery-D3hlyOfw.js +1 -0
- evalkit/bundled/ui/assets/powershell-DmHpPYUd.js +1 -0
- evalkit/bundled/ui/assets/protobuf-C531GsRP.js +2 -0
- evalkit/bundled/ui/assets/pug-Z5eAx3Zn.js +1 -0
- evalkit/bundled/ui/assets/python-CieHVM9n.js +1 -0
- evalkit/bundled/ui/assets/qsharp-DkqhCAOL.js +1 -0
- evalkit/bundled/ui/assets/r-BwWrilGY.js +1 -0
- evalkit/bundled/ui/assets/razor-sLewSMyp.js +1 -0
- evalkit/bundled/ui/assets/redis-ClamHrr6.js +1 -0
- evalkit/bundled/ui/assets/redshift-DT7zqm-g.js +1 -0
- evalkit/bundled/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- evalkit/bundled/ui/assets/ruby-DezsRK8O.js +1 -0
- evalkit/bundled/ui/assets/rust-DdL9SqIa.js +1 -0
- evalkit/bundled/ui/assets/sb-CcwsVR0C.js +1 -0
- evalkit/bundled/ui/assets/scala-DHpiXF5c.js +1 -0
- evalkit/bundled/ui/assets/scheme-BeGwcela.js +1 -0
- evalkit/bundled/ui/assets/scss-gp-XZpBa.js +3 -0
- evalkit/bundled/ui/assets/shell-CC2rA5mh.js +1 -0
- evalkit/bundled/ui/assets/solidity-BEEn4gHE.js +1 -0
- evalkit/bundled/ui/assets/sophia-CRfGWb83.js +1 -0
- evalkit/bundled/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- evalkit/bundled/ui/assets/sql-NEE52Syq.js +1 -0
- evalkit/bundled/ui/assets/st-DbInun42.js +1 -0
- evalkit/bundled/ui/assets/swift-Bxkupp3x.js +1 -0
- evalkit/bundled/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- evalkit/bundled/ui/assets/tcl-DISqw1ZD.js +1 -0
- evalkit/bundled/ui/assets/ts.worker-DIAQkoB3.js +67741 -0
- evalkit/bundled/ui/assets/tsMode-BcVQ1nu0.js +11 -0
- evalkit/bundled/ui/assets/twig-De2hgUGE.js +1 -0
- evalkit/bundled/ui/assets/typescript-CUvNWd9p.js +1 -0
- evalkit/bundled/ui/assets/typespec-B8J7ngcE.js +1 -0
- evalkit/bundled/ui/assets/vb-DV3o63ZY.js +1 -0
- evalkit/bundled/ui/assets/wgsl-DpFanUEy.js +298 -0
- evalkit/bundled/ui/assets/workers-CwpYQtzp.js +1 -0
- evalkit/bundled/ui/assets/xml-C1HnXMdF.js +1 -0
- evalkit/bundled/ui/assets/yaml-D7Ey3HQu.js +1 -0
- evalkit/bundled/ui/favicon.svg +6 -0
- evalkit/bundled/ui/index.html +14 -0
- evalkit/cli/__init__.py +0 -0
- evalkit/cli/admin_commands.py +590 -0
- evalkit/cli/client.py +67 -0
- evalkit/cli/init_server.py +165 -0
- evalkit/cli/local_run.py +195 -0
- evalkit/cli/main.py +1365 -0
- evalkit/cli/runs_commands.py +539 -0
- evalkit/cli/server_commands.py +130 -0
- evalkit/cli/skill_targets.py +43 -0
- evalkit/cli/tokens_commands.py +52 -0
- evalkit/cli/traces_commands.py +332 -0
- evalkit/cli/update.py +85 -0
- evalkit/client/__init__.py +0 -0
- evalkit/client/agents.py +25 -0
- evalkit/client/api.py +193 -0
- evalkit/client/blob.py +104 -0
- evalkit/client/connection.py +88 -0
- evalkit/client/evaluators.py +11 -0
- evalkit/client/sessions.py +17 -0
- evalkit/client/store.py +139 -0
- evalkit/client/summary.py +46 -0
- evalkit/client/sync.py +198 -0
- evalkit/config.py +227 -0
- evalkit/contracts/__init__.py +0 -0
- evalkit/contracts/atif.py +115 -0
- evalkit/contracts/cases.py +456 -0
- evalkit/contracts/custom_evaluator.py +58 -0
- evalkit/contracts/enums.py +86 -0
- evalkit/contracts/esf.py +120 -0
- evalkit/contracts/harness.py +281 -0
- evalkit/contracts/notes.json +4 -0
- evalkit/contracts/notes.py +7 -0
- evalkit/contracts/settings.py +41 -0
- evalkit/core/__init__.py +0 -0
- evalkit/core/access.py +75 -0
- evalkit/core/agents.py +65 -0
- evalkit/core/blob.py +46 -0
- evalkit/core/capabilities.py +36 -0
- evalkit/core/costmap.py +90 -0
- evalkit/core/custom_eval.py +50 -0
- evalkit/core/data/cost_map.json +81546 -0
- evalkit/core/endpoints.py +31 -0
- evalkit/core/evaluators/__init__.py +29 -0
- evalkit/core/evaluators/base.py +121 -0
- evalkit/core/evaluators/gauges.py +84 -0
- evalkit/core/evaluators/presentation.py +166 -0
- evalkit/core/evaluators/response_time.py +21 -0
- evalkit/core/evaluators/rubrics.py +494 -0
- evalkit/core/evaluators/tool_trajectory.py +187 -0
- evalkit/core/evaluators/trajectory_rubrics.py +198 -0
- evalkit/core/fixtures.py +57 -0
- evalkit/core/harness.py +227 -0
- evalkit/core/harness_check.py +191 -0
- evalkit/core/importer.py +257 -0
- evalkit/core/judge.py +78 -0
- evalkit/core/lint.py +265 -0
- evalkit/core/overlays.py +76 -0
- evalkit/core/paths.py +25 -0
- evalkit/core/promotion.py +94 -0
- evalkit/core/pull.py +198 -0
- evalkit/core/python_eval.py +59 -0
- evalkit/core/remote_evaluators.py +32 -0
- evalkit/core/review.py +220 -0
- evalkit/core/run_digest.py +421 -0
- evalkit/core/runner.py +995 -0
- evalkit/core/scorecard.py +255 -0
- evalkit/core/traces/__init__.py +0 -0
- evalkit/core/traces/convert.py +336 -0
- evalkit/core/traces/importer.py +113 -0
- evalkit/core/traces/mapping.py +385 -0
- evalkit/core/traces/poll.py +38 -0
- evalkit/core/traces/raw.py +21 -0
- evalkit/core/traces/sessions.py +172 -0
- evalkit/core/traces/settle.py +14 -0
- evalkit/core/traces/sources/__init__.py +19 -0
- evalkit/core/traces/sources/base.py +25 -0
- evalkit/core/traces/sources/phoenix.py +121 -0
- evalkit/core/trajectory.py +248 -0
- evalkit/core/workspace.py +30 -0
- evalkit/credentials.py +119 -0
- evalkit/db/__init__.py +0 -0
- evalkit/db/agents.py +289 -0
- evalkit/db/alembic/env.py +90 -0
- evalkit/db/alembic/script.py.mako +28 -0
- evalkit/db/alembic/versions/0011_baseline.py +1002 -0
- evalkit/db/alembic/versions/0012_ticket_token_kind.py +27 -0
- evalkit/db/alembic/versions/0013_keys_and_llm_providers.py +67 -0
- evalkit/db/cases.py +608 -0
- evalkit/db/crypto.py +12 -0
- evalkit/db/evaluators.py +156 -0
- evalkit/db/experiments.py +363 -0
- evalkit/db/fixture_blobs.py +91 -0
- evalkit/db/keys.py +78 -0
- evalkit/db/llm_providers.py +176 -0
- evalkit/db/locks.py +6 -0
- evalkit/db/mappings.py +168 -0
- evalkit/db/models.py +567 -0
- evalkit/db/promotion.py +271 -0
- evalkit/db/rekey.py +46 -0
- evalkit/db/review.py +250 -0
- evalkit/db/review_state.py +50 -0
- evalkit/db/runs.py +533 -0
- evalkit/db/scorecard.py +300 -0
- evalkit/db/session.py +71 -0
- evalkit/db/sessions.py +90 -0
- evalkit/db/spans.py +360 -0
- evalkit/db/store.py +209 -0
- evalkit/db/tokens.py +100 -0
- evalkit/db/trace_imports.py +308 -0
- evalkit/db/trace_sources.py +131 -0
- evalkit/db/users.py +186 -0
- evalkit/db/workspaces.py +232 -0
- evalkit/paths.py +33 -0
- evalkit/sdk/__init__.py +29 -0
- evalkit/sdk/binding.py +69 -0
- evalkit/sdk/evaluators.py +56 -0
- evalkit/sdk/runner.py +957 -0
- evalkit/sdk/tracing.py +166 -0
- evalkit/store/__init__.py +0 -0
- evalkit/store/base.py +354 -0
- evalkit/store/blob.py +69 -0
- evalkit/version.py +44 -0
- evalkit_py-0.2.0.dev12.dist-info/METADATA +205 -0
- evalkit_py-0.2.0.dev12.dist-info/RECORD +371 -0
- evalkit_py-0.2.0.dev12.dist-info/WHEEL +4 -0
- evalkit_py-0.2.0.dev12.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|