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
evalkit/core/runner.py
ADDED
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
"""The experiment runner: `prepare_experiment` pins the selection and creates
|
|
2
|
+
the record (server only; it needs the review state), then `drive_experiment`
|
|
3
|
+
(phase 1: task runs) and `evaluate_experiment` (phase 2: scoring) run
|
|
4
|
+
anywhere a `Store` and a `BlobStore` are available. Both phases are idempotent so
|
|
5
|
+
a failed experiment resumes instead of restarting.
|
|
6
|
+
|
|
7
|
+
Phase 1 fans agent runs out to the harness under a concurrency cap, persists
|
|
8
|
+
each trajectory and the run's artifacts to the blob store at completion, and
|
|
9
|
+
records the run. Phase 2 walks recorded runs and executes configured
|
|
10
|
+
built-ins, configured custom evaluators, and every gauge, publishing evaluation
|
|
11
|
+
rows to the store. The runner holds a lease on the experiment throughout and
|
|
12
|
+
renews it while it works.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import tempfile
|
|
21
|
+
import time
|
|
22
|
+
import uuid
|
|
23
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
24
|
+
from contextlib import ExitStack
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import UTC, datetime
|
|
27
|
+
from typing import IO, Literal, Protocol
|
|
28
|
+
|
|
29
|
+
import httpx
|
|
30
|
+
from pydantic import BaseModel, ValidationError
|
|
31
|
+
|
|
32
|
+
from evalkit.config import RunnerConfig
|
|
33
|
+
from evalkit.contracts.atif import Trajectory, TrajectoryValidationError, validate_trajectory
|
|
34
|
+
from evalkit.contracts.custom_evaluator import EvalCase, ScoreItem
|
|
35
|
+
from evalkit.contracts.enums import (
|
|
36
|
+
EvaluationSource,
|
|
37
|
+
EvaluatorKind,
|
|
38
|
+
ExperimentStatus,
|
|
39
|
+
RunStatus,
|
|
40
|
+
TrajectoryStatus,
|
|
41
|
+
)
|
|
42
|
+
from evalkit.contracts.harness import (
|
|
43
|
+
Capabilities,
|
|
44
|
+
FileRef,
|
|
45
|
+
RunMetadata,
|
|
46
|
+
answer_text,
|
|
47
|
+
input_errors,
|
|
48
|
+
)
|
|
49
|
+
from evalkit.contracts.settings import JudgeSettings
|
|
50
|
+
from evalkit.core.agents import AgentDefinition, AgentLookup, explicit
|
|
51
|
+
from evalkit.core.blob import (
|
|
52
|
+
BlobStore,
|
|
53
|
+
artifact_key,
|
|
54
|
+
fixture_blob_key,
|
|
55
|
+
trajectory_key,
|
|
56
|
+
workspace_key,
|
|
57
|
+
)
|
|
58
|
+
from evalkit.core.capabilities import traced_capabilities
|
|
59
|
+
from evalkit.core.costmap import load_cost_map
|
|
60
|
+
from evalkit.core.custom_eval import EvalError, score_with_custom_evaluator
|
|
61
|
+
from evalkit.core.evaluators.base import (
|
|
62
|
+
BUILTIN_EVALUATORS,
|
|
63
|
+
BUILTIN_GAUGES,
|
|
64
|
+
EvalContext,
|
|
65
|
+
execute,
|
|
66
|
+
neutral_skip,
|
|
67
|
+
)
|
|
68
|
+
from evalkit.core.harness import HarnessClient, RunTimeout
|
|
69
|
+
from evalkit.core.paths import is_safe_path
|
|
70
|
+
from evalkit.core.python_eval import PythonEvaluator, score_with_python_evaluator
|
|
71
|
+
from evalkit.core.remote_evaluators import EvaluatorLookup, RemoteEvaluator
|
|
72
|
+
from evalkit.core.traces.poll import poll_trajectory
|
|
73
|
+
from evalkit.core.traces.sessions import SessionReader
|
|
74
|
+
from evalkit.core.trajectory import (
|
|
75
|
+
RunMetrics,
|
|
76
|
+
TrajectoryView,
|
|
77
|
+
backfill_step_costs,
|
|
78
|
+
derive_metrics,
|
|
79
|
+
)
|
|
80
|
+
from evalkit.core.workspace import parse_slug
|
|
81
|
+
from evalkit.credentials import KeySource
|
|
82
|
+
from evalkit.store.base import (
|
|
83
|
+
AgentRunRecord,
|
|
84
|
+
CaseView,
|
|
85
|
+
DatasetRow,
|
|
86
|
+
EvaluationRow,
|
|
87
|
+
Experiment,
|
|
88
|
+
ExperimentConfig,
|
|
89
|
+
ExperimentSnapshot,
|
|
90
|
+
GitRevision,
|
|
91
|
+
NewExperiment,
|
|
92
|
+
PinnedVersion,
|
|
93
|
+
RunForScoring,
|
|
94
|
+
ScoreRow,
|
|
95
|
+
Selection,
|
|
96
|
+
Store,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
LOGGER = logging.getLogger(__name__)
|
|
100
|
+
|
|
101
|
+
LEASE_SECONDS = 60
|
|
102
|
+
RENEW_EVERY_SECONDS = 15
|
|
103
|
+
ACTIVE = frozenset({ExperimentStatus.RUNNING, ExperimentStatus.EVALUATING})
|
|
104
|
+
ALLOWED: frozenset[tuple[ExperimentStatus | None, ExperimentStatus]] = frozenset(
|
|
105
|
+
{
|
|
106
|
+
(None, ExperimentStatus.RUNNING),
|
|
107
|
+
(ExperimentStatus.RUNNING, ExperimentStatus.EVALUATING),
|
|
108
|
+
(ExperimentStatus.EVALUATING, ExperimentStatus.COMPLETED),
|
|
109
|
+
(ExperimentStatus.RUNNING, ExperimentStatus.FAILED),
|
|
110
|
+
(ExperimentStatus.EVALUATING, ExperimentStatus.FAILED),
|
|
111
|
+
(ExperimentStatus.FAILED, ExperimentStatus.RUNNING),
|
|
112
|
+
(ExperimentStatus.COMPLETED, ExperimentStatus.RUNNING),
|
|
113
|
+
(ExperimentStatus.COMPLETED, ExperimentStatus.EVALUATING),
|
|
114
|
+
(ExperimentStatus.FAILED, ExperimentStatus.EVALUATING),
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ExperimentRequest(BaseModel):
|
|
120
|
+
dataset: str
|
|
121
|
+
agent: str
|
|
122
|
+
selection: Selection = Selection(all=True)
|
|
123
|
+
repetitions: int = 1
|
|
124
|
+
model: str | None = None
|
|
125
|
+
capabilities: Capabilities | None = None
|
|
126
|
+
cases: GitRevision | None = None
|
|
127
|
+
evalkit_version: str | None = None
|
|
128
|
+
code_evaluators: list[str] | None = None
|
|
129
|
+
judge: JudgeSettings | None = None
|
|
130
|
+
holder: str | None = None
|
|
131
|
+
trajectory_wait_seconds: int | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _no_progress(message: str) -> None:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AgentKeySource(Protocol):
|
|
139
|
+
async def agent_key(self, agent: AgentDefinition) -> str | None: ...
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class RunnerDeps:
|
|
144
|
+
cfg: RunnerConfig
|
|
145
|
+
store: Store
|
|
146
|
+
blob_store: BlobStore
|
|
147
|
+
agents: AgentLookup
|
|
148
|
+
evaluators: EvaluatorLookup
|
|
149
|
+
credentials: KeySource
|
|
150
|
+
harness_transport: httpx.AsyncBaseTransport | None = None
|
|
151
|
+
custom_eval_transport: httpx.AsyncBaseTransport | None = None
|
|
152
|
+
progress: Callable[[str], None] = _no_progress
|
|
153
|
+
sessions: SessionReader | None = None
|
|
154
|
+
python_evaluators: dict[str, PythonEvaluator] = field(default_factory=dict)
|
|
155
|
+
agent_keys: AgentKeySource | None = None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ExperimentFactory(Protocol):
|
|
159
|
+
async def dataset(self, ref: str) -> DatasetRow: ...
|
|
160
|
+
|
|
161
|
+
async def cases_for(
|
|
162
|
+
self, dataset: str, selection: Selection, *, agent: str | None
|
|
163
|
+
) -> list[CaseView]: ...
|
|
164
|
+
|
|
165
|
+
async def create_experiment(self, req: NewExperiment) -> Experiment: ...
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def _spool_fixtures(
|
|
169
|
+
deps: RunnerDeps, case: CaseView, stack: ExitStack
|
|
170
|
+
) -> list[tuple[str, IO[bytes]]]:
|
|
171
|
+
files: list[tuple[str, IO[bytes]]] = []
|
|
172
|
+
for file in case.files:
|
|
173
|
+
spool = stack.enter_context(tempfile.TemporaryFile())
|
|
174
|
+
blob = await deps.blob_store.get_stream(fixture_blob_key(file.sha256))
|
|
175
|
+
async for chunk in blob.chunks:
|
|
176
|
+
await asyncio.to_thread(spool.write, chunk)
|
|
177
|
+
spool.seek(0)
|
|
178
|
+
files.append((file.key, spool))
|
|
179
|
+
return files
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def _fixture_refs(deps: RunnerDeps, case: CaseView, *, with_urls: bool) -> list[FileRef]:
|
|
183
|
+
return [
|
|
184
|
+
FileRef(
|
|
185
|
+
name=file.key,
|
|
186
|
+
url=await deps.blob_store.object_url(fixture_blob_key(file.sha256))
|
|
187
|
+
if with_urls
|
|
188
|
+
else None,
|
|
189
|
+
context=file.context,
|
|
190
|
+
)
|
|
191
|
+
for file in case.files
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class AgentArchived(ValueError):
|
|
196
|
+
def __init__(self, ref: str):
|
|
197
|
+
super().__init__(f"agent {ref} is archived")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class NothingRunnable(ValueError):
|
|
201
|
+
def __init__(self, dataset: str, dropped: list[str]):
|
|
202
|
+
reason = (
|
|
203
|
+
"every selected case needs fixture files and the harness accepts none: "
|
|
204
|
+
+ ", ".join(dropped)
|
|
205
|
+
if dropped
|
|
206
|
+
else "no cases match the selection"
|
|
207
|
+
)
|
|
208
|
+
super().__init__(f"nothing to run in {dataset}: {reason}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class InvalidCaseInputs(Exception):
|
|
212
|
+
def __init__(self, agent: str, bad: dict[str, str]):
|
|
213
|
+
self.agent = agent
|
|
214
|
+
self.bad = bad
|
|
215
|
+
lines = "\n".join(f" {case_id}: {error}" for case_id, error in sorted(bad.items()))
|
|
216
|
+
super().__init__(f"{len(bad)} case input(s) fail agent {agent}'s input_schema:\n{lines}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class LeaseLost(ValueError):
|
|
220
|
+
def __init__(self, exp_id: int):
|
|
221
|
+
self.exp_id = exp_id
|
|
222
|
+
super().__init__(
|
|
223
|
+
f"experiment {exp_id} was taken over by another runner or marked failed; "
|
|
224
|
+
f"check it with evalkit status {exp_id}, then resume it"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def error_text(exc: BaseException) -> str:
|
|
229
|
+
message = str(exc)
|
|
230
|
+
return f"{type(exc).__name__}: {message}" if message else type(exc).__name__
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class ScoringFailed(Exception):
|
|
234
|
+
def __init__(self, exp_id: int, failed: int, scored: int, first: BaseException):
|
|
235
|
+
self.exp_id = exp_id
|
|
236
|
+
super().__init__(
|
|
237
|
+
f"scoring failed for {failed} of {failed + scored} runs of experiment {exp_id}; "
|
|
238
|
+
f"first error: {error_text(first)}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def case_title(case_input: object) -> str | None:
|
|
243
|
+
question = case_input.get("question") if isinstance(case_input, dict) else None
|
|
244
|
+
if not isinstance(question, str) or not question.strip():
|
|
245
|
+
return None
|
|
246
|
+
return question.strip().splitlines()[0][:200]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
async def _persist_artifacts(
|
|
250
|
+
deps: RunnerDeps,
|
|
251
|
+
client: HarnessClient,
|
|
252
|
+
harness_run_id: str,
|
|
253
|
+
refs: list[dict],
|
|
254
|
+
) -> None:
|
|
255
|
+
for ref in refs:
|
|
256
|
+
path = ref.get("path")
|
|
257
|
+
if not path:
|
|
258
|
+
continue
|
|
259
|
+
if not is_safe_path(path):
|
|
260
|
+
LOGGER.warning(
|
|
261
|
+
"artifact %s for run %s has an unsafe path; skipped", path, harness_run_id
|
|
262
|
+
)
|
|
263
|
+
continue
|
|
264
|
+
try:
|
|
265
|
+
data = await client.get_file(harness_run_id, path)
|
|
266
|
+
except httpx.HTTPError as exc:
|
|
267
|
+
LOGGER.warning("artifact %s for run %s unavailable: %s", path, harness_run_id, exc)
|
|
268
|
+
continue
|
|
269
|
+
await deps.blob_store.put_object(artifact_key(client.agent.id, harness_run_id, path), data)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
async def _persist_workspace_tree(
|
|
273
|
+
deps: RunnerDeps, client: HarnessClient, harness_run_id: str
|
|
274
|
+
) -> None:
|
|
275
|
+
try:
|
|
276
|
+
tree = await client.list_files(harness_run_id)
|
|
277
|
+
except httpx.HTTPError as exc:
|
|
278
|
+
LOGGER.warning("workspace tree for run %s unavailable: %s", harness_run_id, exc)
|
|
279
|
+
return
|
|
280
|
+
await deps.blob_store.put_object(
|
|
281
|
+
workspace_key(client.agent.id, harness_run_id), tree.model_dump_json().encode()
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class DrivenRun:
|
|
287
|
+
case: CaseView
|
|
288
|
+
repetition: int
|
|
289
|
+
started: datetime
|
|
290
|
+
harness_run_id: str
|
|
291
|
+
answer: str | None = None
|
|
292
|
+
output: dict | None = None
|
|
293
|
+
error: str | None = None
|
|
294
|
+
run_status: RunStatus = RunStatus.FAILED
|
|
295
|
+
metrics: dict = field(default_factory=dict)
|
|
296
|
+
reported_model: str | None = None
|
|
297
|
+
trajectory: Trajectory | None = None
|
|
298
|
+
trajectory_status: TrajectoryStatus | None = None
|
|
299
|
+
wall_ms: int = 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def _drive_one(
|
|
303
|
+
deps: RunnerDeps,
|
|
304
|
+
exp: Experiment,
|
|
305
|
+
client: HarnessClient,
|
|
306
|
+
caps: Capabilities,
|
|
307
|
+
case: CaseView,
|
|
308
|
+
repetition: int,
|
|
309
|
+
model: str | None,
|
|
310
|
+
) -> DrivenRun:
|
|
311
|
+
started = datetime.now(UTC)
|
|
312
|
+
clock = time.monotonic()
|
|
313
|
+
file_refs: list[FileRef] | None = None
|
|
314
|
+
if case.files:
|
|
315
|
+
file_refs = await _fixture_refs(deps, case, with_urls=caps.accepts.file_urls)
|
|
316
|
+
with ExitStack() as stack:
|
|
317
|
+
files = (
|
|
318
|
+
await _spool_fixtures(deps, case, stack)
|
|
319
|
+
if case.files and not caps.accepts.file_urls
|
|
320
|
+
else None
|
|
321
|
+
)
|
|
322
|
+
harness_run_id = await client.start_run(
|
|
323
|
+
case.input,
|
|
324
|
+
files=files,
|
|
325
|
+
file_refs=file_refs,
|
|
326
|
+
model=model,
|
|
327
|
+
metadata=RunMetadata(
|
|
328
|
+
experiment_id=str(exp.id), case_id=case.case_id, repetition=repetition
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
driven = DrivenRun(
|
|
332
|
+
case=case, repetition=repetition, started=started, harness_run_id=harness_run_id
|
|
333
|
+
)
|
|
334
|
+
try:
|
|
335
|
+
status = await client.poll_until_terminal(harness_run_id)
|
|
336
|
+
driven.answer = status.answer
|
|
337
|
+
driven.output = status.output
|
|
338
|
+
driven.error = status.error
|
|
339
|
+
driven.run_status = RunStatus(status.status)
|
|
340
|
+
driven.reported_model = status.model
|
|
341
|
+
if status.status != "completed" and driven.error is None:
|
|
342
|
+
driven.error = f"run ended with status {status.status}"
|
|
343
|
+
if status.artifacts:
|
|
344
|
+
driven.metrics["artifacts"] = [
|
|
345
|
+
a.model_dump(exclude_none=True) for a in status.artifacts
|
|
346
|
+
]
|
|
347
|
+
except RunTimeout:
|
|
348
|
+
driven.run_status = RunStatus.TIMEOUT
|
|
349
|
+
driven.error = f"run timed out after {client.agent.settings.run_timeout_seconds}s"
|
|
350
|
+
except ValidationError as exc:
|
|
351
|
+
driven.error = f"harness protocol violation: {exc.errors()[0]['msg']}"
|
|
352
|
+
driven.wall_ms = int((time.monotonic() - clock) * 1000)
|
|
353
|
+
fetch = not client.agent.is_otel and caps.provides.trajectory
|
|
354
|
+
if fetch and driven.error != "cancelled":
|
|
355
|
+
try:
|
|
356
|
+
driven.trajectory = await client.trajectory(harness_run_id)
|
|
357
|
+
driven.trajectory_status = TrajectoryStatus.VALID
|
|
358
|
+
except TrajectoryValidationError as exc:
|
|
359
|
+
driven.trajectory_status = TrajectoryStatus.INVALID
|
|
360
|
+
LOGGER.warning("trajectory for run %s failed ATIF validation: %s", harness_run_id, exc)
|
|
361
|
+
except httpx.HTTPError as exc:
|
|
362
|
+
driven.trajectory_status = TrajectoryStatus.UNAVAILABLE
|
|
363
|
+
LOGGER.warning("trajectory fetch for run %s failed: %s", harness_run_id, exc)
|
|
364
|
+
return driven
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def reused_run_id(harness_run_id: str) -> str:
|
|
368
|
+
return f"{harness_run_id}#reused-{uuid.uuid4().hex[:12]}"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
async def store_trajectory(
|
|
372
|
+
deps: RunnerDeps,
|
|
373
|
+
agent_id: int,
|
|
374
|
+
harness_run_id: str,
|
|
375
|
+
trajectory: Trajectory,
|
|
376
|
+
status: TrajectoryStatus | None,
|
|
377
|
+
model: str | None,
|
|
378
|
+
) -> str:
|
|
379
|
+
backfill_step_costs(trajectory, load_cost_map(deps.cfg.cost_map), model)
|
|
380
|
+
if status == TrajectoryStatus.PARTIAL:
|
|
381
|
+
trajectory.extra = {**(trajectory.extra or {}), "trajectory_status": "partial"}
|
|
382
|
+
key = trajectory_key(agent_id, harness_run_id)
|
|
383
|
+
await deps.blob_store.put_object(
|
|
384
|
+
key, json.dumps(trajectory.model_dump(exclude_none=True)).encode()
|
|
385
|
+
)
|
|
386
|
+
return key
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
async def _record_one(
|
|
390
|
+
deps: RunnerDeps,
|
|
391
|
+
exp: Experiment,
|
|
392
|
+
client: HarnessClient,
|
|
393
|
+
caps: Capabilities,
|
|
394
|
+
driven: DrivenRun,
|
|
395
|
+
holder: str | None,
|
|
396
|
+
) -> str | None:
|
|
397
|
+
agent = client.agent
|
|
398
|
+
used = await deps.store.harness_run_id_used(exp.id, driven.harness_run_id)
|
|
399
|
+
if used is not None:
|
|
400
|
+
driven.harness_run_id = reused_run_id(driven.harness_run_id)
|
|
401
|
+
driven.run_status = RunStatus.FAILED
|
|
402
|
+
driven.error = used
|
|
403
|
+
driven.trajectory = None
|
|
404
|
+
driven.trajectory_status = TrajectoryStatus.UNAVAILABLE
|
|
405
|
+
driven.metrics.pop("artifacts", None)
|
|
406
|
+
elif agent.is_otel and driven.error != "cancelled":
|
|
407
|
+
if deps.sessions is None:
|
|
408
|
+
raise ValueError(
|
|
409
|
+
f"agent {agent.name} takes its trajectory from traces but no session reader "
|
|
410
|
+
"is configured"
|
|
411
|
+
)
|
|
412
|
+
doc, status = await poll_trajectory(
|
|
413
|
+
deps.sessions,
|
|
414
|
+
exp.agent,
|
|
415
|
+
driven.harness_run_id,
|
|
416
|
+
agent.settings.trajectory_wait_seconds,
|
|
417
|
+
)
|
|
418
|
+
driven.trajectory_status = TrajectoryStatus(status)
|
|
419
|
+
if doc is not None:
|
|
420
|
+
driven.trajectory = validate_trajectory(doc)
|
|
421
|
+
key: str | None = None
|
|
422
|
+
if driven.trajectory is not None:
|
|
423
|
+
key = await store_trajectory(
|
|
424
|
+
deps,
|
|
425
|
+
agent.id,
|
|
426
|
+
driven.harness_run_id,
|
|
427
|
+
driven.trajectory,
|
|
428
|
+
driven.trajectory_status,
|
|
429
|
+
exp.config.model or driven.reported_model,
|
|
430
|
+
)
|
|
431
|
+
view = TrajectoryView(driven.trajectory)
|
|
432
|
+
driven.metrics.update(derive_metrics(view, driven.wall_ms).model_dump(exclude_none=True))
|
|
433
|
+
driven.metrics.setdefault("duration_ms", driven.wall_ms)
|
|
434
|
+
await deps.store.record_run(
|
|
435
|
+
exp.id,
|
|
436
|
+
driven.case.case_id,
|
|
437
|
+
driven.repetition,
|
|
438
|
+
AgentRunRecord(
|
|
439
|
+
harness_run_id=driven.harness_run_id,
|
|
440
|
+
status=driven.run_status,
|
|
441
|
+
answer=driven.answer,
|
|
442
|
+
output=driven.output,
|
|
443
|
+
error=driven.error,
|
|
444
|
+
started_at=driven.started,
|
|
445
|
+
ended_at=datetime.now(UTC),
|
|
446
|
+
trajectory_key=key,
|
|
447
|
+
trajectory_status=driven.trajectory_status,
|
|
448
|
+
metrics=driven.metrics,
|
|
449
|
+
title=case_title(driven.case.input),
|
|
450
|
+
),
|
|
451
|
+
holder=holder,
|
|
452
|
+
)
|
|
453
|
+
deps.progress(f"run {driven.case.case_id} #{driven.repetition} {driven.run_status}")
|
|
454
|
+
if caps.provides.workspace and driven.metrics.get("artifacts"):
|
|
455
|
+
await _persist_artifacts(deps, client, driven.harness_run_id, driven.metrics["artifacts"])
|
|
456
|
+
if caps.provides.workspace and used is None:
|
|
457
|
+
await _persist_workspace_tree(deps, client, driven.harness_run_id)
|
|
458
|
+
return driven.reported_model
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def dropped_cases_text(dropped: list[str]) -> str:
|
|
462
|
+
return (
|
|
463
|
+
f"skipped {len(dropped)} case(s) with fixture files; the agent accepts neither "
|
|
464
|
+
f"files nor file urls: {', '.join(dropped)}"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def runnable(cases: list[CaseView], caps: Capabilities) -> tuple[list[CaseView], list[str]]:
|
|
469
|
+
if caps.accepts.file_input:
|
|
470
|
+
return cases, []
|
|
471
|
+
gated = sorted(case.case_id for case in cases if case.files)
|
|
472
|
+
return [case for case in cases if not case.files], gated
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _check_inputs(agent: str, caps: Capabilities, cases: list[CaseView]) -> None:
|
|
476
|
+
bad = {}
|
|
477
|
+
for case in cases:
|
|
478
|
+
errors = input_errors(caps.effective_input_schema, case.input)
|
|
479
|
+
if errors:
|
|
480
|
+
bad[case.case_id] = errors[0]
|
|
481
|
+
if bad:
|
|
482
|
+
raise InvalidCaseInputs(agent, bad)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
async def harness_capabilities(
|
|
486
|
+
agent: AgentDefinition, api_key: str | None, transport: httpx.AsyncBaseTransport | None
|
|
487
|
+
) -> Capabilities:
|
|
488
|
+
client = HarnessClient(agent, api_key, transport=transport)
|
|
489
|
+
try:
|
|
490
|
+
return traced_capabilities(await client.capabilities(), agent)
|
|
491
|
+
finally:
|
|
492
|
+
await client.aclose()
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
async def prepare_experiment(
|
|
496
|
+
factory: ExperimentFactory,
|
|
497
|
+
req: ExperimentRequest,
|
|
498
|
+
*,
|
|
499
|
+
agent: AgentDefinition,
|
|
500
|
+
caps: Capabilities,
|
|
501
|
+
remote_evaluators: dict[str, RemoteEvaluator],
|
|
502
|
+
judge: JudgeSettings,
|
|
503
|
+
runner: str,
|
|
504
|
+
holder: str,
|
|
505
|
+
revision: GitRevision | None = None,
|
|
506
|
+
) -> tuple[Experiment, list[str]]:
|
|
507
|
+
model = req.model
|
|
508
|
+
if model is not None and not caps.accepts.model_override:
|
|
509
|
+
LOGGER.warning(
|
|
510
|
+
"harness %s does not accept model override — running with its default model",
|
|
511
|
+
req.agent,
|
|
512
|
+
)
|
|
513
|
+
model = None
|
|
514
|
+
dataset = await factory.dataset(req.dataset)
|
|
515
|
+
cases = await factory.cases_for(dataset.ref, req.selection, agent=agent.ref)
|
|
516
|
+
cases, dropped = runnable(cases, caps)
|
|
517
|
+
if not cases:
|
|
518
|
+
raise NothingRunnable(dataset.ref, dropped)
|
|
519
|
+
_check_inputs(req.agent, caps, cases)
|
|
520
|
+
config = ExperimentConfig(
|
|
521
|
+
model=model,
|
|
522
|
+
judge=judge,
|
|
523
|
+
selection=req.selection,
|
|
524
|
+
revision=req.cases or revision,
|
|
525
|
+
snapshot=ExperimentSnapshot(
|
|
526
|
+
capabilities=caps.model_dump(),
|
|
527
|
+
trajectory_source=agent.trajectory_source,
|
|
528
|
+
trajectory_wait_seconds=agent.settings.trajectory_wait_seconds
|
|
529
|
+
if req.trajectory_wait_seconds is None
|
|
530
|
+
else req.trajectory_wait_seconds,
|
|
531
|
+
remote_evaluators={
|
|
532
|
+
name: evaluator.settings for name, evaluator in remote_evaluators.items()
|
|
533
|
+
},
|
|
534
|
+
code_evaluators=sorted(req.code_evaluators or []),
|
|
535
|
+
evalkit_version=req.evalkit_version,
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
exp = await factory.create_experiment(
|
|
539
|
+
NewExperiment(
|
|
540
|
+
dataset=dataset.ref,
|
|
541
|
+
agent=agent.ref,
|
|
542
|
+
runner=runner,
|
|
543
|
+
repetitions=req.repetitions,
|
|
544
|
+
config=config,
|
|
545
|
+
cases=[
|
|
546
|
+
PinnedVersion(case_ref=case.case_ref, content_hash=case.content_hash)
|
|
547
|
+
for case in cases
|
|
548
|
+
],
|
|
549
|
+
holder=holder,
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
return exp, dropped
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
async def _agent_for(deps: RunnerDeps, exp: Experiment) -> AgentDefinition:
|
|
556
|
+
agent = await deps.agents.get(explicit(parse_slug(exp.agent)))
|
|
557
|
+
if agent.archived:
|
|
558
|
+
raise AgentArchived(agent.ref)
|
|
559
|
+
return agent
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
async def missing_local_keys(
|
|
563
|
+
deps: RunnerDeps, agent: AgentDefinition, cases: Iterable[CaseView]
|
|
564
|
+
) -> list[str]:
|
|
565
|
+
used = {name for case in cases for name in case.effective_evaluators.get("custom") or {}}
|
|
566
|
+
lines = []
|
|
567
|
+
if agent.harness_url is not None and deps.credentials.key_for("agent", agent.ref) is None:
|
|
568
|
+
lines.append(
|
|
569
|
+
f"no credentials.agents entry for {agent.ref} in evalkit.yaml; "
|
|
570
|
+
"calling the harness without x-api-key"
|
|
571
|
+
)
|
|
572
|
+
for name, evaluator in (await deps.evaluators.in_workspace(agent.workspace)).items():
|
|
573
|
+
if name in used and deps.credentials.key_for("evaluator", evaluator.ref) is None:
|
|
574
|
+
lines.append(
|
|
575
|
+
f"no credentials.evaluators entry for {evaluator.ref} in evalkit.yaml; "
|
|
576
|
+
"calling it without x-api-key"
|
|
577
|
+
)
|
|
578
|
+
return lines
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
async def drive_experiment(deps: RunnerDeps, exp_id: int, holder: str | None = None) -> None:
|
|
582
|
+
exp = await deps.store.get_experiment(exp_id)
|
|
583
|
+
agent = await _agent_for(deps, exp)
|
|
584
|
+
client = HarnessClient(
|
|
585
|
+
agent,
|
|
586
|
+
deps.credentials.key_for("agent", agent.ref, agent.harness_url),
|
|
587
|
+
transport=deps.harness_transport,
|
|
588
|
+
)
|
|
589
|
+
# Runs execute under the experiment's snapshotted contract (capabilities,
|
|
590
|
+
# model, pinned cases), never a fresh harness call, so resumed runs are
|
|
591
|
+
# gated and modelled like the rest of the experiment.
|
|
592
|
+
caps = Capabilities.model_validate(exp.config.snapshot.capabilities)
|
|
593
|
+
model = exp.config.model
|
|
594
|
+
try:
|
|
595
|
+
cases = {case.case_id: case for case in await deps.store.pinned_cases(exp_id)}
|
|
596
|
+
judge_key(deps, exp.config.judge, cases.values())
|
|
597
|
+
limit = agent.settings.max_concurrent_runs
|
|
598
|
+
if caps.limits.max_concurrent_runs is not None:
|
|
599
|
+
limit = min(limit, caps.limits.max_concurrent_runs)
|
|
600
|
+
semaphore = asyncio.Semaphore(max(limit, 1))
|
|
601
|
+
reported_models: list[str | None] = []
|
|
602
|
+
|
|
603
|
+
async def bounded(case: CaseView, repetition: int) -> None:
|
|
604
|
+
async with semaphore:
|
|
605
|
+
driven = await _drive_one(deps, exp, client, caps, case, repetition, model)
|
|
606
|
+
reported_models.append(await _record_one(deps, exp, client, caps, driven, holder))
|
|
607
|
+
|
|
608
|
+
await asyncio.gather(
|
|
609
|
+
*(
|
|
610
|
+
bounded(cases[pending.case_id], pending.repetition)
|
|
611
|
+
for pending in await deps.store.pending_runs(exp_id)
|
|
612
|
+
)
|
|
613
|
+
)
|
|
614
|
+
if model is None:
|
|
615
|
+
observed = next((m for m in reported_models if m), None)
|
|
616
|
+
if observed is not None:
|
|
617
|
+
await deps.store.set_model(exp_id, observed)
|
|
618
|
+
finally:
|
|
619
|
+
await client.aclose()
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
async def _renew_forever(store: Store, exp_id: int, holder: str) -> None:
|
|
623
|
+
renewed_at = time.monotonic()
|
|
624
|
+
while True:
|
|
625
|
+
await asyncio.sleep(RENEW_EVERY_SECONDS)
|
|
626
|
+
try:
|
|
627
|
+
renewed = await store.renew_lease(exp_id, holder)
|
|
628
|
+
except Exception as exc:
|
|
629
|
+
if time.monotonic() - renewed_at >= LEASE_SECONDS:
|
|
630
|
+
raise LeaseLost(exp_id) from exc
|
|
631
|
+
LOGGER.warning("renewing the lease on experiment %s failed: %s", exp_id, exc)
|
|
632
|
+
continue
|
|
633
|
+
if not renewed:
|
|
634
|
+
LOGGER.warning("lost lease on experiment %s", exp_id)
|
|
635
|
+
raise LeaseLost(exp_id)
|
|
636
|
+
renewed_at = time.monotonic()
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
async def with_lease(store: Store, exp_id: int, holder: str, work: Awaitable[None]) -> None:
|
|
640
|
+
renew = asyncio.create_task(_renew_forever(store, exp_id, holder))
|
|
641
|
+
main = asyncio.ensure_future(work)
|
|
642
|
+
try:
|
|
643
|
+
await asyncio.wait({main, renew}, return_when=asyncio.FIRST_COMPLETED)
|
|
644
|
+
if not main.done():
|
|
645
|
+
main.cancel()
|
|
646
|
+
await asyncio.gather(main, return_exceptions=True)
|
|
647
|
+
renew.result()
|
|
648
|
+
main.result()
|
|
649
|
+
finally:
|
|
650
|
+
renew.cancel()
|
|
651
|
+
main.cancel()
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
async def run_leased(deps: RunnerDeps, exp_id: int, holder: str, work: Awaitable[None]) -> None:
|
|
655
|
+
try:
|
|
656
|
+
await with_lease(deps.store, exp_id, holder, work)
|
|
657
|
+
except LeaseLost:
|
|
658
|
+
raise
|
|
659
|
+
except Exception as exc:
|
|
660
|
+
await deps.store.transition(exp_id, ExperimentStatus.FAILED, holder, error=error_text(exc))
|
|
661
|
+
raise
|
|
662
|
+
await deps.store.transition(exp_id, ExperimentStatus.COMPLETED, holder)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
async def run_experiment(deps: RunnerDeps, exp_id: int, holder: str) -> None:
|
|
666
|
+
async def work() -> None:
|
|
667
|
+
await drive_experiment(deps, exp_id, holder)
|
|
668
|
+
await deps.store.transition(exp_id, ExperimentStatus.EVALUATING, holder)
|
|
669
|
+
await evaluate_experiment(deps, exp_id, holder)
|
|
670
|
+
|
|
671
|
+
await run_leased(deps, exp_id, holder, work())
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
async def rescore_experiment(
|
|
675
|
+
deps: RunnerDeps, exp_id: int, holder: str, *, refetch_trajectories: bool = False
|
|
676
|
+
) -> None:
|
|
677
|
+
await run_leased(
|
|
678
|
+
deps,
|
|
679
|
+
exp_id,
|
|
680
|
+
holder,
|
|
681
|
+
evaluate_experiment(deps, exp_id, holder, refetch_trajectories=refetch_trajectories),
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def judge_key_for(
|
|
686
|
+
credentials: KeySource, judge: JudgeSettings, cases: Iterable[CaseView]
|
|
687
|
+
) -> str | None:
|
|
688
|
+
needed = any(
|
|
689
|
+
BUILTIN_EVALUATORS[name].uses_judge
|
|
690
|
+
for case in cases
|
|
691
|
+
for name in _configured_builtins(case.effective_evaluators)
|
|
692
|
+
)
|
|
693
|
+
return credentials.require_judge_key(judge.base_url) if needed else None
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def judge_key(deps: RunnerDeps, judge: JudgeSettings, cases: Iterable[CaseView]) -> str | None:
|
|
697
|
+
return judge_key_for(deps.credentials, judge, cases)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _configured_builtins(evaluators: dict) -> dict[str, dict]:
|
|
701
|
+
configured: dict[str, dict] = {}
|
|
702
|
+
for family in ("output", "trajectory"):
|
|
703
|
+
for name, config in (evaluators.get(family) or {}).items():
|
|
704
|
+
if name in BUILTIN_EVALUATORS:
|
|
705
|
+
configured[name] = config
|
|
706
|
+
return configured
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
REFETCHABLE = (TrajectoryStatus.PARTIAL, TrajectoryStatus.UNAVAILABLE)
|
|
710
|
+
REFETCH_CONCURRENCY = 8
|
|
711
|
+
SCORABLE = (TrajectoryStatus.VALID, TrajectoryStatus.PARTIAL)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
async def stored_trajectory_doc(
|
|
715
|
+
blob_store: BlobStore, agent_id: int, run: AgentRunRecord
|
|
716
|
+
) -> tuple[dict | None, TrajectoryStatus | None]:
|
|
717
|
+
status = run.trajectory_status
|
|
718
|
+
if status in REFETCHABLE:
|
|
719
|
+
candidate = trajectory_key(agent_id, run.harness_run_id)
|
|
720
|
+
if await blob_store.object_exists(candidate):
|
|
721
|
+
doc = json.loads(await blob_store.get_object(candidate))
|
|
722
|
+
return doc, TrajectoryStatus((doc.get("extra") or {}).get("trajectory_status", "valid"))
|
|
723
|
+
if run.trajectory_key and status in SCORABLE:
|
|
724
|
+
return json.loads(await blob_store.get_object(run.trajectory_key)), status
|
|
725
|
+
return None, status
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
async def _load_view(deps: RunnerDeps, agent_id: int, run: AgentRunRecord) -> TrajectoryView | None:
|
|
729
|
+
doc, status = await stored_trajectory_doc(deps.blob_store, agent_id, run)
|
|
730
|
+
if doc is None or status not in SCORABLE:
|
|
731
|
+
return None
|
|
732
|
+
return TrajectoryView(validate_trajectory(doc))
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _rows(
|
|
736
|
+
producer: str, source: EvaluationSource, scores: Iterable[ScoreRow]
|
|
737
|
+
) -> list[EvaluationRow]:
|
|
738
|
+
return [
|
|
739
|
+
EvaluationRow(
|
|
740
|
+
producer=producer,
|
|
741
|
+
name=score.name,
|
|
742
|
+
kind=EvaluatorKind.GAUGE if score.kind == "gauge" else EvaluatorKind.SCORE,
|
|
743
|
+
source=source,
|
|
744
|
+
value=score.score,
|
|
745
|
+
label=score.label,
|
|
746
|
+
explanation=score.explanation,
|
|
747
|
+
error=score.error,
|
|
748
|
+
)
|
|
749
|
+
for score in scores
|
|
750
|
+
]
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _custom_rows(
|
|
754
|
+
name: str,
|
|
755
|
+
kind: Literal["evaluator", "gauge"],
|
|
756
|
+
source: EvaluationSource,
|
|
757
|
+
outcome: list[ScoreItem] | EvalError,
|
|
758
|
+
) -> list[EvaluationRow]:
|
|
759
|
+
if isinstance(outcome, EvalError):
|
|
760
|
+
return _rows(
|
|
761
|
+
name,
|
|
762
|
+
source,
|
|
763
|
+
[ScoreRow(name=name, kind=kind, score=None, label="error", error=outcome.message)],
|
|
764
|
+
)
|
|
765
|
+
return _rows(
|
|
766
|
+
name,
|
|
767
|
+
source,
|
|
768
|
+
[
|
|
769
|
+
ScoreRow(
|
|
770
|
+
name=item.name,
|
|
771
|
+
kind=kind,
|
|
772
|
+
score=item.score,
|
|
773
|
+
label=item.label,
|
|
774
|
+
explanation=item.explanation,
|
|
775
|
+
)
|
|
776
|
+
for item in outcome
|
|
777
|
+
],
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def custom_case_input(case: CaseView) -> dict:
|
|
782
|
+
if not case.files:
|
|
783
|
+
return dict(case.input)
|
|
784
|
+
return {**case.input, "files": [file.model_dump() for file in case.files]}
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
async def _evaluate_run(
|
|
788
|
+
deps: RunnerDeps,
|
|
789
|
+
exp: Experiment,
|
|
790
|
+
agent: AgentDefinition,
|
|
791
|
+
caps: Capabilities,
|
|
792
|
+
cost_map: dict,
|
|
793
|
+
run: RunForScoring,
|
|
794
|
+
judge_semaphore: asyncio.Semaphore,
|
|
795
|
+
judge_api_key: str | None,
|
|
796
|
+
configured_custom: dict[str, RemoteEvaluator],
|
|
797
|
+
holder: str | None,
|
|
798
|
+
*,
|
|
799
|
+
force: bool = False,
|
|
800
|
+
) -> None:
|
|
801
|
+
record = run.agent_run
|
|
802
|
+
structured = record.output
|
|
803
|
+
answer = (
|
|
804
|
+
answer_text(record.answer, structured) if structured is not None else record.answer or ""
|
|
805
|
+
)
|
|
806
|
+
metrics_dict = dict(record.metrics)
|
|
807
|
+
view = await _load_view(deps, agent.id, record)
|
|
808
|
+
if view is not None:
|
|
809
|
+
wall = metrics_dict.get("duration_ms")
|
|
810
|
+
metrics_dict = {**metrics_dict, **derive_metrics(view, wall).model_dump(exclude_none=True)}
|
|
811
|
+
ctx = EvalContext(
|
|
812
|
+
case=run.case,
|
|
813
|
+
answer=answer,
|
|
814
|
+
output=structured,
|
|
815
|
+
trajectory_view=view,
|
|
816
|
+
wall_ms=metrics_dict.get("duration_ms") or 0,
|
|
817
|
+
judge_cfg=exp.config.judge,
|
|
818
|
+
judge_api_key=judge_api_key,
|
|
819
|
+
metrics=RunMetrics.model_validate(metrics_dict) if metrics_dict else None,
|
|
820
|
+
session_id=record.harness_run_id,
|
|
821
|
+
cost_map=cost_map,
|
|
822
|
+
)
|
|
823
|
+
already = set() if force else run.done
|
|
824
|
+
rows: list[EvaluationRow] = []
|
|
825
|
+
evaluators = run.case.effective_evaluators
|
|
826
|
+
|
|
827
|
+
for name, config in _configured_builtins(evaluators).items():
|
|
828
|
+
evaluator = BUILTIN_EVALUATORS[name]
|
|
829
|
+
if set(evaluator.emits) <= already:
|
|
830
|
+
continue
|
|
831
|
+
async with judge_semaphore:
|
|
832
|
+
try:
|
|
833
|
+
scores = await execute(evaluator, ctx, config, caps)
|
|
834
|
+
except Exception as exc:
|
|
835
|
+
LOGGER.warning(
|
|
836
|
+
"evaluator %s errored on run %s: %s", name, run.experiment_run_id, exc
|
|
837
|
+
)
|
|
838
|
+
scores = [ScoreRow(name=name, score=None, label="error", error=str(exc))]
|
|
839
|
+
rows.extend(_rows(name, EvaluationSource.BUILTIN, scores))
|
|
840
|
+
|
|
841
|
+
custom_config = evaluators.get("custom") or {}
|
|
842
|
+
trajectory_doc: dict = {}
|
|
843
|
+
if view is not None:
|
|
844
|
+
trajectory_doc = view.trajectory.model_dump(exclude_none=True)
|
|
845
|
+
eval_case = EvalCase(
|
|
846
|
+
id=run.case.case_id,
|
|
847
|
+
input=custom_case_input(run.case),
|
|
848
|
+
expected=run.case.expected,
|
|
849
|
+
config={},
|
|
850
|
+
)
|
|
851
|
+
for name, config in custom_config.items():
|
|
852
|
+
if name in already:
|
|
853
|
+
continue
|
|
854
|
+
python_evaluator = deps.python_evaluators.get(name)
|
|
855
|
+
if python_evaluator is not None:
|
|
856
|
+
if python_evaluator.requires_trajectory and view is None:
|
|
857
|
+
rows.extend(
|
|
858
|
+
_rows(
|
|
859
|
+
name, EvaluationSource.CODE, [neutral_skip(name, "run has no trajectory")]
|
|
860
|
+
)
|
|
861
|
+
)
|
|
862
|
+
continue
|
|
863
|
+
outcome = await score_with_python_evaluator(
|
|
864
|
+
python_evaluator,
|
|
865
|
+
case=eval_case.model_copy(update={"config": config}),
|
|
866
|
+
answer=answer,
|
|
867
|
+
output=structured,
|
|
868
|
+
trajectory=trajectory_doc,
|
|
869
|
+
)
|
|
870
|
+
rows.extend(_custom_rows(name, python_evaluator.kind, EvaluationSource.CODE, outcome))
|
|
871
|
+
continue
|
|
872
|
+
evaluator_cfg = configured_custom.get(name)
|
|
873
|
+
if evaluator_cfg is None:
|
|
874
|
+
rows.extend(
|
|
875
|
+
_rows(
|
|
876
|
+
name,
|
|
877
|
+
EvaluationSource.REMOTE,
|
|
878
|
+
[
|
|
879
|
+
ScoreRow(
|
|
880
|
+
name=name,
|
|
881
|
+
score=None,
|
|
882
|
+
label="error",
|
|
883
|
+
error=f"custom evaluator {name} is not configured",
|
|
884
|
+
)
|
|
885
|
+
],
|
|
886
|
+
)
|
|
887
|
+
)
|
|
888
|
+
continue
|
|
889
|
+
result = await score_with_custom_evaluator(
|
|
890
|
+
evaluator_cfg,
|
|
891
|
+
deps.credentials.key_for(
|
|
892
|
+
"evaluator", evaluator_cfg.ref, evaluator_cfg.settings.endpoint
|
|
893
|
+
),
|
|
894
|
+
case=eval_case.model_copy(update={"config": config}),
|
|
895
|
+
answer=answer,
|
|
896
|
+
output=structured,
|
|
897
|
+
trajectory=trajectory_doc,
|
|
898
|
+
transport=deps.custom_eval_transport,
|
|
899
|
+
)
|
|
900
|
+
rows.extend(_custom_rows(name, evaluator_cfg.wire_kind, EvaluationSource.REMOTE, result))
|
|
901
|
+
|
|
902
|
+
for name, gauge in BUILTIN_GAUGES.items():
|
|
903
|
+
if name in already:
|
|
904
|
+
continue
|
|
905
|
+
rows.extend(_rows(name, EvaluationSource.BUILTIN, await execute(gauge, ctx, {}, caps)))
|
|
906
|
+
|
|
907
|
+
if rows:
|
|
908
|
+
await deps.store.publish_evaluations(
|
|
909
|
+
run.experiment_run_id, rows, force=force, holder=holder
|
|
910
|
+
)
|
|
911
|
+
deps.progress(f"scored {run.case.case_id} #{run.repetition} ({len(rows)} rows)")
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
async def _refetch(
|
|
915
|
+
deps: RunnerDeps, exp: Experiment, agent: AgentDefinition, runs: list[RunForScoring]
|
|
916
|
+
) -> set[int]:
|
|
917
|
+
if deps.sessions is None:
|
|
918
|
+
raise ValueError("refetching trajectories needs a session reader")
|
|
919
|
+
wait = exp.config.snapshot.trajectory_wait_seconds
|
|
920
|
+
refetched: set[int] = set()
|
|
921
|
+
slots = asyncio.Semaphore(REFETCH_CONCURRENCY)
|
|
922
|
+
sessions = deps.sessions
|
|
923
|
+
|
|
924
|
+
async def one(run: RunForScoring) -> None:
|
|
925
|
+
async with slots:
|
|
926
|
+
doc, status = await poll_trajectory(
|
|
927
|
+
sessions, exp.agent, run.agent_run.harness_run_id, wait
|
|
928
|
+
)
|
|
929
|
+
if doc is None:
|
|
930
|
+
return
|
|
931
|
+
trajectory = validate_trajectory(doc)
|
|
932
|
+
trajectory.extra = {**(trajectory.extra or {}), "trajectory_status": status}
|
|
933
|
+
await store_trajectory(
|
|
934
|
+
deps,
|
|
935
|
+
agent.id,
|
|
936
|
+
run.agent_run.harness_run_id,
|
|
937
|
+
trajectory,
|
|
938
|
+
TrajectoryStatus(status),
|
|
939
|
+
exp.config.model,
|
|
940
|
+
)
|
|
941
|
+
refetched.add(run.experiment_run_id)
|
|
942
|
+
deps.progress(f"refetched trajectory for {run.case.case_id} #{run.repetition}: {status}")
|
|
943
|
+
|
|
944
|
+
await asyncio.gather(
|
|
945
|
+
*(one(run) for run in runs if run.agent_run.trajectory_status in REFETCHABLE)
|
|
946
|
+
)
|
|
947
|
+
return refetched
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
async def evaluate_experiment(
|
|
951
|
+
deps: RunnerDeps,
|
|
952
|
+
exp_id: int,
|
|
953
|
+
holder: str | None = None,
|
|
954
|
+
*,
|
|
955
|
+
refetch_trajectories: bool = False,
|
|
956
|
+
) -> None:
|
|
957
|
+
exp = await deps.store.get_experiment(exp_id)
|
|
958
|
+
agent = await deps.agents.get(explicit(parse_slug(exp.agent)))
|
|
959
|
+
caps = Capabilities.model_validate(exp.config.snapshot.capabilities)
|
|
960
|
+
cost_map = load_cost_map(deps.cfg.cost_map)
|
|
961
|
+
runs = await deps.store.runs_to_score(exp_id, force=False)
|
|
962
|
+
refetched = await _refetch(deps, exp, agent, runs) if refetch_trajectories else set()
|
|
963
|
+
judge_semaphore = asyncio.Semaphore(exp.config.judge.concurrency)
|
|
964
|
+
api_key = judge_key(deps, exp.config.judge, [run.case for run in runs])
|
|
965
|
+
configured_custom = (
|
|
966
|
+
await deps.evaluators.in_workspace(agent.workspace)
|
|
967
|
+
if any(run.case.effective_evaluators.get("custom") for run in runs)
|
|
968
|
+
else {}
|
|
969
|
+
)
|
|
970
|
+
results = await asyncio.gather(
|
|
971
|
+
*(
|
|
972
|
+
_evaluate_run(
|
|
973
|
+
deps,
|
|
974
|
+
exp,
|
|
975
|
+
agent,
|
|
976
|
+
caps,
|
|
977
|
+
cost_map,
|
|
978
|
+
run,
|
|
979
|
+
judge_semaphore,
|
|
980
|
+
api_key,
|
|
981
|
+
configured_custom,
|
|
982
|
+
holder,
|
|
983
|
+
force=run.experiment_run_id in refetched,
|
|
984
|
+
)
|
|
985
|
+
for run in runs
|
|
986
|
+
),
|
|
987
|
+
return_exceptions=True,
|
|
988
|
+
)
|
|
989
|
+
errors = [r for r in results if isinstance(r, BaseException)]
|
|
990
|
+
for error in errors:
|
|
991
|
+
if not isinstance(error, Exception):
|
|
992
|
+
raise error
|
|
993
|
+
if errors:
|
|
994
|
+
first = errors[0]
|
|
995
|
+
raise ScoringFailed(exp_id, len(errors), len(results) - len(errors), first) from first
|