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