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/api/mcp/tools.py
ADDED
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from datetime import date, datetime, timedelta, tzinfo
|
|
6
|
+
from typing import Any, get_args
|
|
7
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from fastapi import HTTPException
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
from fastmcp.exceptions import ToolError
|
|
13
|
+
from fastmcp.server.auth import require_scopes
|
|
14
|
+
from mcp.types import ToolAnnotations
|
|
15
|
+
from starlette.datastructures import State
|
|
16
|
+
|
|
17
|
+
from evalkit.api import experiments as experiments_api
|
|
18
|
+
from evalkit.api import runs as runs_api
|
|
19
|
+
from evalkit.api.agents import agent_view
|
|
20
|
+
from evalkit.api.mcp import descriptions as d
|
|
21
|
+
from evalkit.api.mcp.auth import SCOPE_READ
|
|
22
|
+
from evalkit.api.mcp.models import (
|
|
23
|
+
AgentRow,
|
|
24
|
+
BatchMeta,
|
|
25
|
+
DownloadRunFileResponse,
|
|
26
|
+
Evaluation,
|
|
27
|
+
ExperimentRow,
|
|
28
|
+
ExperimentRunRow,
|
|
29
|
+
ExperimentRunsMeta,
|
|
30
|
+
FileRow,
|
|
31
|
+
FileText,
|
|
32
|
+
FindAgentsResponse,
|
|
33
|
+
FindExperimentsResponse,
|
|
34
|
+
FindRunsResponse,
|
|
35
|
+
GetExperimentRunsResponse,
|
|
36
|
+
GetExperimentsResponse,
|
|
37
|
+
GetRunFilesResponse,
|
|
38
|
+
GetRunsResponse,
|
|
39
|
+
GetRunStatsResponse,
|
|
40
|
+
GetTrajectoryResponse,
|
|
41
|
+
ModelCostRow,
|
|
42
|
+
Money,
|
|
43
|
+
Note,
|
|
44
|
+
PagedMeta,
|
|
45
|
+
PlainMeta,
|
|
46
|
+
RunMetrics,
|
|
47
|
+
RunRow,
|
|
48
|
+
StatsMeta,
|
|
49
|
+
StatsRow,
|
|
50
|
+
SubagentRef,
|
|
51
|
+
ToolCallRow,
|
|
52
|
+
TrajectoryMeta,
|
|
53
|
+
TurnRow,
|
|
54
|
+
usd,
|
|
55
|
+
)
|
|
56
|
+
from evalkit.contracts.atif import Step, Trajectory
|
|
57
|
+
from evalkit.contracts.enums import ExperimentStatus
|
|
58
|
+
from evalkit.core.agents import AgentDefinition, UnknownAgent
|
|
59
|
+
from evalkit.core.costmap import load_cost_map
|
|
60
|
+
from evalkit.core.paths import is_safe_run_id
|
|
61
|
+
from evalkit.core.run_digest import (
|
|
62
|
+
GROUP_BY_VALUES,
|
|
63
|
+
GroupStats,
|
|
64
|
+
RunDigest,
|
|
65
|
+
StatsInput,
|
|
66
|
+
Turn,
|
|
67
|
+
clip,
|
|
68
|
+
content_text,
|
|
69
|
+
digest,
|
|
70
|
+
group_stats,
|
|
71
|
+
priced_steps,
|
|
72
|
+
turns,
|
|
73
|
+
)
|
|
74
|
+
from evalkit.core.traces.sessions import SessionTooLarge
|
|
75
|
+
from evalkit.core.trajectory import backfill_step_costs
|
|
76
|
+
from evalkit.core.workspace import parse_slug
|
|
77
|
+
from evalkit.db import experiments as db_experiments
|
|
78
|
+
from evalkit.db import scorecard as db_scorecard
|
|
79
|
+
from evalkit.db.runs import RunSource, trajectory_keys
|
|
80
|
+
from evalkit.db.workspaces import UnknownWorkspace
|
|
81
|
+
from evalkit.store.base import UnknownExperiment
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _registration(open_world: bool) -> dict[str, Any]:
|
|
85
|
+
return {
|
|
86
|
+
"annotations": ToolAnnotations(
|
|
87
|
+
read_only_hint=True, destructive_hint=False, open_world_hint=open_world
|
|
88
|
+
),
|
|
89
|
+
"meta": {"anthropic/alwaysLoad": True},
|
|
90
|
+
"auth": require_scopes(SCOPE_READ),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Open world: the tool reads the agent's harness live, not only evalkit's store.
|
|
95
|
+
READ_TOOL = _registration(open_world=False)
|
|
96
|
+
HARNESS_READ_TOOL = _registration(open_world=True)
|
|
97
|
+
|
|
98
|
+
FIND_RUNS_MAX = 200
|
|
99
|
+
FIND_RUNS_LIST_MAX = 5000
|
|
100
|
+
GET_RUNS_MAX = 200
|
|
101
|
+
STATS_MAX_RUNS = 5000
|
|
102
|
+
STEP_PAGE_MAX = 100
|
|
103
|
+
FILE_CHARS = 20000
|
|
104
|
+
FILE_CHARS_MAX = 200000
|
|
105
|
+
EXPERIMENTS_MAX = 100
|
|
106
|
+
GET_EXPERIMENTS_MAX = 20
|
|
107
|
+
# Per call, so one heavy call cannot queue ahead of a light one; the server-wide
|
|
108
|
+
# cap bounds what every call together puts on harnesses and the event loop.
|
|
109
|
+
FETCH_CONCURRENCY = 32
|
|
110
|
+
FETCH_CONCURRENCY_TOTAL = 64
|
|
111
|
+
# Whole call, listing included: under the 60 s load-balancer timeout.
|
|
112
|
+
CALL_BUDGET_SECONDS = 45
|
|
113
|
+
OVER_BUDGET = "not read within this call's time budget; ask for it again with get_runs"
|
|
114
|
+
|
|
115
|
+
LOGGER = logging.getLogger(__name__)
|
|
116
|
+
RUN_INCLUDES = frozenset({"metrics", "turns", "tool_calls"})
|
|
117
|
+
RUN_SOURCES: tuple[str, ...] = get_args(RunSource)
|
|
118
|
+
EXPERIMENT_INCLUDES = frozenset({"scorecard"})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
STATUS_CODES = {
|
|
122
|
+
400: "invalid_request",
|
|
123
|
+
401: "forbidden",
|
|
124
|
+
403: "forbidden",
|
|
125
|
+
404: "not_found",
|
|
126
|
+
409: "conflict",
|
|
127
|
+
413: "invalid_request",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def fail(code: str, detail: str, log_level: int = logging.WARNING) -> ToolError:
|
|
132
|
+
return ToolError(f"{code}: {detail}", log_level=log_level)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def translate(exc: Exception) -> ToolError | None:
|
|
136
|
+
if isinstance(exc, HTTPException):
|
|
137
|
+
code = STATUS_CODES.get(exc.status_code, "upstream_failed")
|
|
138
|
+
level = logging.ERROR if exc.status_code == 500 else logging.WARNING
|
|
139
|
+
return fail(code, str(exc.detail), level)
|
|
140
|
+
if isinstance(exc, (UnknownAgent, UnknownExperiment, UnknownWorkspace)):
|
|
141
|
+
return fail("not_found", exc.args[0])
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# FastMCP logs any exception other than ToolError at ERROR before middleware
|
|
146
|
+
# sees it, so expected errors become ToolError inside the tool.
|
|
147
|
+
def expected_errors[**P, R](tool: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
|
148
|
+
@functools.wraps(tool)
|
|
149
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
150
|
+
try:
|
|
151
|
+
return await tool(*args, **kwargs)
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
translated = translate(exc)
|
|
154
|
+
if translated is None:
|
|
155
|
+
raise
|
|
156
|
+
raise translated from exc
|
|
157
|
+
|
|
158
|
+
return wrapper
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def deadline() -> float:
|
|
162
|
+
return asyncio.get_running_loop().time() + CALL_BUDGET_SECONDS
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def check_run_ids(run_ids: list[str]) -> None:
|
|
166
|
+
unsafe = [r for r in run_ids if not is_safe_run_id(r)]
|
|
167
|
+
if unsafe:
|
|
168
|
+
raise fail("invalid_run_id", f"run ids must be one plain path segment: {unsafe[:5]}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_agent_id(ref: str) -> str:
|
|
172
|
+
try:
|
|
173
|
+
parse_slug(ref)
|
|
174
|
+
except ValueError as exc:
|
|
175
|
+
raise fail(
|
|
176
|
+
"invalid_agent_id", f"{ref!r}: use the name@workspace id from find_agents"
|
|
177
|
+
) from exc
|
|
178
|
+
return ref
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def check_choice(code: str, value: str, allowed: tuple[str, ...]) -> None:
|
|
182
|
+
if value not in allowed:
|
|
183
|
+
raise fail(code, f"{value!r}; valid: {list(allowed)}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def check_page(limit: int, offset: int, maximum: int) -> None:
|
|
187
|
+
if not 1 <= limit <= maximum:
|
|
188
|
+
raise fail("invalid_limit", f"limit must be between 1 and {maximum}; got {limit}")
|
|
189
|
+
if offset < 0:
|
|
190
|
+
raise fail("invalid_offset", f"offset must be 0 or greater; got {offset}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def check_ids(ids: list[Any], maximum: int) -> None:
|
|
194
|
+
if not ids:
|
|
195
|
+
raise fail("no_ids_provided", "pass at least one id")
|
|
196
|
+
if len(ids) > maximum:
|
|
197
|
+
raise fail("too_many_ids", f"at most {maximum} ids per call; got {len(ids)}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def check_include(include: list[str], allowed: frozenset[str]) -> set[str]:
|
|
201
|
+
unknown = sorted(set(include) - allowed)
|
|
202
|
+
if unknown:
|
|
203
|
+
raise fail("invalid_include", f"{unknown}; valid: {sorted(allowed)}")
|
|
204
|
+
return set(include)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def zone_of(name: str) -> ZoneInfo:
|
|
208
|
+
try:
|
|
209
|
+
return ZoneInfo(name)
|
|
210
|
+
except (ZoneInfoNotFoundError, ValueError) as exc:
|
|
211
|
+
raise fail("invalid_timezone", f"{name} is not an IANA timezone") from exc
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _bound(value: str, zone: tzinfo, *, end: bool) -> datetime:
|
|
215
|
+
try:
|
|
216
|
+
if len(value) == 10:
|
|
217
|
+
day = date.fromisoformat(value)
|
|
218
|
+
moment = datetime(day.year, day.month, day.day, tzinfo=zone)
|
|
219
|
+
return moment + timedelta(days=1) if end else moment
|
|
220
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
221
|
+
except ValueError as exc:
|
|
222
|
+
raise fail(
|
|
223
|
+
"invalid_date_format", f"{value!r}: use YYYY-MM-DD or an ISO 8601 timestamp"
|
|
224
|
+
) from exc
|
|
225
|
+
return parsed if parsed.tzinfo else parsed.replace(tzinfo=zone)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def window(
|
|
229
|
+
start_date: str | None, end_date: str | None, zone: tzinfo
|
|
230
|
+
) -> tuple[datetime | None, datetime | None]:
|
|
231
|
+
since = _bound(start_date, zone, end=False) if start_date else None
|
|
232
|
+
until = _bound(end_date, zone, end=True) if end_date else None
|
|
233
|
+
if since and until and since >= until:
|
|
234
|
+
raise fail("invalid_range", "start_date must be before end_date")
|
|
235
|
+
return since, until
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def paged_meta(
|
|
239
|
+
count: int, total: int | None, offset: int, has_more: bool | None = None
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
if has_more is None:
|
|
242
|
+
has_more = total is not None and offset + count < total
|
|
243
|
+
return {
|
|
244
|
+
"count": count,
|
|
245
|
+
"total": total,
|
|
246
|
+
"has_more": has_more,
|
|
247
|
+
"next_offset": offset + count if has_more else None,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def run_metrics(result: RunDigest) -> RunMetrics:
|
|
252
|
+
fields = result.model_dump(exclude={"run_id", "cost_usd", "tool_call_groups", "cost_by_model"})
|
|
253
|
+
by_model = [
|
|
254
|
+
ModelCostRow(**m.model_dump(exclude={"cost_usd"}), cost=Money(value=m.cost_usd))
|
|
255
|
+
for m in result.cost_by_model
|
|
256
|
+
]
|
|
257
|
+
return RunMetrics(**fields, cost=usd(result.cost_usd), cost_by_model=by_model)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def turn_row(turn: Turn) -> TurnRow:
|
|
261
|
+
return TurnRow(**turn.model_dump(exclude={"cost_usd"}), cost=usd(turn.cost_usd))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def run_row(run: runs_api.BrowserRun) -> RunRow:
|
|
265
|
+
return RunRow(
|
|
266
|
+
id=run.run_id,
|
|
267
|
+
title=run.title,
|
|
268
|
+
created_at=run.created_at,
|
|
269
|
+
user_id=run.user_id,
|
|
270
|
+
client_id=run.client_id,
|
|
271
|
+
source=run.source,
|
|
272
|
+
status=run.status,
|
|
273
|
+
experiment_id=run.experiment_id,
|
|
274
|
+
experiment_number=run.experiment_number,
|
|
275
|
+
case_id=run.case_id,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def stats_row(stats: GroupStats) -> StatsRow:
|
|
280
|
+
return StatsRow(**stats.model_dump(exclude={"cost_usd"}), cost=Money(value=stats.cost_usd))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def clip_value(value: Any, max_chars: int) -> Any:
|
|
284
|
+
return value if len(repr(value)) <= max_chars else clip(str(value), max_chars)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def clipped_step(step: Step, max_chars: int) -> dict:
|
|
288
|
+
out = step.model_dump(exclude_none=True)
|
|
289
|
+
if step.message is not None:
|
|
290
|
+
out["message"] = clip(content_text(step.message), max_chars)
|
|
291
|
+
if step.reasoning_content is not None:
|
|
292
|
+
out["reasoning_content"] = clip(step.reasoning_content, max_chars)
|
|
293
|
+
for call in out.get("tool_calls") or []:
|
|
294
|
+
call["arguments"] = clip_value(call["arguments"], max_chars)
|
|
295
|
+
if step.observation is not None:
|
|
296
|
+
results = out["observation"]["results"]
|
|
297
|
+
for result, raw in zip(results, step.observation.results, strict=True):
|
|
298
|
+
if raw.content is not None:
|
|
299
|
+
result["content"] = clip(content_text(raw.content), max_chars)
|
|
300
|
+
return out
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
Digested = tuple[RunDigest, list[Turn]]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class Runs:
|
|
307
|
+
def __init__(self, state: State) -> None:
|
|
308
|
+
self.state = state
|
|
309
|
+
self.gate = asyncio.Semaphore(FETCH_CONCURRENCY_TOTAL)
|
|
310
|
+
|
|
311
|
+
async def agent(self, ref: str) -> AgentDefinition:
|
|
312
|
+
return await runs_api.agent_or_404(self.state.agents, check_agent_id(ref))
|
|
313
|
+
|
|
314
|
+
async def cost_map(self) -> dict:
|
|
315
|
+
return await asyncio.to_thread(load_cost_map, self.state.cfg.cost_map)
|
|
316
|
+
|
|
317
|
+
async def trajectory(self, record: AgentDefinition, run_id: str) -> tuple[Trajectory, set]:
|
|
318
|
+
async with self.state.session_factory() as session:
|
|
319
|
+
trajectory = await runs_api.fetch_trajectory(
|
|
320
|
+
record, run_id, self.state.runner_deps, self.state.sessions, session
|
|
321
|
+
)
|
|
322
|
+
reported = priced_steps(trajectory)
|
|
323
|
+
backfill_step_costs(trajectory, await self.cost_map())
|
|
324
|
+
return trajectory, reported
|
|
325
|
+
|
|
326
|
+
async def digests(
|
|
327
|
+
self, record: AgentDefinition, run_ids: list[str], budget: float
|
|
328
|
+
) -> list[Digested | HTTPException]:
|
|
329
|
+
# One stored-run lookup and one cost map per call, and no DB connection
|
|
330
|
+
# held across harness calls; whatever is not read within the budget
|
|
331
|
+
# comes back as a per-run failure rather than outliving the request.
|
|
332
|
+
async with self.state.session_factory() as session:
|
|
333
|
+
keys = await trajectory_keys(session, record.id, run_ids)
|
|
334
|
+
cost_map = await self.cost_map()
|
|
335
|
+
deps = self.state.runner_deps
|
|
336
|
+
client = (
|
|
337
|
+
await runs_api.harness_client(deps, record)
|
|
338
|
+
if runs_api.server_may_drive(record)
|
|
339
|
+
else None
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
call_gate = asyncio.Semaphore(FETCH_CONCURRENCY)
|
|
343
|
+
|
|
344
|
+
async def one(run_id: str) -> Digested:
|
|
345
|
+
async with call_gate, self.gate:
|
|
346
|
+
trajectory = await runs_api.read_trajectory(
|
|
347
|
+
record, run_id, deps, self.state.sessions, keys.get(run_id), client
|
|
348
|
+
)
|
|
349
|
+
reported = priced_steps(trajectory)
|
|
350
|
+
backfill_step_costs(trajectory, cost_map)
|
|
351
|
+
return digest(run_id, trajectory, reported), turns(trajectory)
|
|
352
|
+
|
|
353
|
+
tasks = [asyncio.create_task(one(run_id)) for run_id in run_ids]
|
|
354
|
+
try:
|
|
355
|
+
remaining = budget - asyncio.get_running_loop().time()
|
|
356
|
+
if tasks and remaining > 0:
|
|
357
|
+
await asyncio.wait(tasks, timeout=remaining)
|
|
358
|
+
finally:
|
|
359
|
+
for task in tasks:
|
|
360
|
+
task.cancel()
|
|
361
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
362
|
+
if client is not None:
|
|
363
|
+
await client.aclose()
|
|
364
|
+
return [outcome(task) for task in tasks]
|
|
365
|
+
|
|
366
|
+
async def page(
|
|
367
|
+
self, record: AgentDefinition, *, limit: int, offset: int, budget: float, **filters: Any
|
|
368
|
+
) -> runs_api.RunPage:
|
|
369
|
+
try:
|
|
370
|
+
async with asyncio.timeout_at(budget), self.state.session_factory() as session:
|
|
371
|
+
return await runs_api.resolve_runs(
|
|
372
|
+
record,
|
|
373
|
+
self.state.runner_deps,
|
|
374
|
+
self.state.spans,
|
|
375
|
+
session,
|
|
376
|
+
limit=limit,
|
|
377
|
+
offset=offset,
|
|
378
|
+
**filters,
|
|
379
|
+
)
|
|
380
|
+
except TimeoutError as exc:
|
|
381
|
+
raise fail(
|
|
382
|
+
"harness_timeout",
|
|
383
|
+
f"listing the agent's runs took longer than {CALL_BUDGET_SECONDS}s; "
|
|
384
|
+
"narrow the window or the filters",
|
|
385
|
+
) from exc
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def outcome(task: asyncio.Task[Digested]) -> Digested | HTTPException:
|
|
389
|
+
if task.cancelled():
|
|
390
|
+
return HTTPException(status_code=504, detail=OVER_BUDGET)
|
|
391
|
+
exc = task.exception()
|
|
392
|
+
if exc is None:
|
|
393
|
+
return task.result()
|
|
394
|
+
if isinstance(exc, HTTPException):
|
|
395
|
+
return exc
|
|
396
|
+
if isinstance(exc, httpx.HTTPError | TimeoutError):
|
|
397
|
+
return HTTPException(status_code=502, detail=f"harness unreachable ({type(exc).__name__})")
|
|
398
|
+
if isinstance(exc, ValueError):
|
|
399
|
+
return HTTPException(status_code=502, detail="malformed trajectory")
|
|
400
|
+
if isinstance(exc, SessionTooLarge):
|
|
401
|
+
return HTTPException(status_code=413, detail="session too large to convert")
|
|
402
|
+
if not isinstance(exc, Exception):
|
|
403
|
+
raise exc
|
|
404
|
+
LOGGER.error("mcp.trajectory_read_failed", exc_info=exc)
|
|
405
|
+
return HTTPException(
|
|
406
|
+
status_code=502, detail=f"trajectory could not be read ({type(exc).__name__})"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# A harness answers an id it does not know with 404, or 400/422 when the id is not
|
|
411
|
+
# even of its shape: either way the run does not exist there.
|
|
412
|
+
NOT_FOUND_STATUSES = frozenset({400, 404, 422})
|
|
413
|
+
ROW_ERROR_CODES = {504: "harness_timeout", 413: "session_too_large"}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def unread(result: Digested | HTTPException) -> bool:
|
|
417
|
+
return isinstance(result, HTTPException) and result.status_code not in NOT_FOUND_STATUSES
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def unread_note(results: list[Digested | HTTPException], truncated: bool) -> Note | None:
|
|
421
|
+
unread_count = sum(1 for r in results if unread(r))
|
|
422
|
+
parts = []
|
|
423
|
+
if truncated:
|
|
424
|
+
parts.append("the window holds more runs than were read")
|
|
425
|
+
if unread_count:
|
|
426
|
+
parts.append(f"{unread_count} runs could not be read (time budget or harness errors)")
|
|
427
|
+
return Note(code="partial_coverage", message="; ".join(parts)) if parts else None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def with_digest(row: RunRow, result: Digested | HTTPException, include: set[str]) -> RunRow:
|
|
431
|
+
if isinstance(result, HTTPException):
|
|
432
|
+
if result.status_code in NOT_FOUND_STATUSES:
|
|
433
|
+
row.note = Note(code="not_found")
|
|
434
|
+
else:
|
|
435
|
+
row.error = ROW_ERROR_CODES.get(result.status_code, "harness_failed")
|
|
436
|
+
row.detail = f"trajectory: {result.detail}"
|
|
437
|
+
return row
|
|
438
|
+
found, run_turns = result
|
|
439
|
+
if "metrics" in include:
|
|
440
|
+
row.metrics = run_metrics(found)
|
|
441
|
+
if "turns" in include:
|
|
442
|
+
row.turns = [turn_row(t) for t in run_turns]
|
|
443
|
+
if "tool_calls" in include:
|
|
444
|
+
row.tool_calls = [ToolCallRow(**g.model_dump()) for g in found.tool_call_groups]
|
|
445
|
+
return row
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def harness_note(page: runs_api.RunPage) -> Note | None:
|
|
449
|
+
if not page.harness_note:
|
|
450
|
+
return None
|
|
451
|
+
return Note(code="partial_coverage", message=page.harness_note)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def register(mcp: FastMCP, state: State) -> None:
|
|
455
|
+
runs = Runs(state)
|
|
456
|
+
|
|
457
|
+
@mcp.tool(description=d.FIND_AGENTS, **READ_TOOL)
|
|
458
|
+
@expected_errors
|
|
459
|
+
async def find_agents(
|
|
460
|
+
workspace: str | None = None, include_archived: bool = False
|
|
461
|
+
) -> FindAgentsResponse:
|
|
462
|
+
found = await state.agents.list_agents(
|
|
463
|
+
workspace=workspace, include_archived=include_archived
|
|
464
|
+
)
|
|
465
|
+
rows = []
|
|
466
|
+
for agent in found:
|
|
467
|
+
view = agent_view(agent)
|
|
468
|
+
rows.append(
|
|
469
|
+
AgentRow(
|
|
470
|
+
id=view["ref"],
|
|
471
|
+
name=view["name"],
|
|
472
|
+
workspace=view["workspace"],
|
|
473
|
+
workspace_type=view["workspace_type"],
|
|
474
|
+
trajectory_source=view["trajectory_source"],
|
|
475
|
+
driver=view["driver"],
|
|
476
|
+
archived=view["archived"],
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
return FindAgentsResponse(data=rows, meta=PlainMeta(count=len(rows)))
|
|
480
|
+
|
|
481
|
+
@mcp.tool(description=d.FIND_RUNS, **HARNESS_READ_TOOL)
|
|
482
|
+
@expected_errors
|
|
483
|
+
async def find_runs(
|
|
484
|
+
agent: str,
|
|
485
|
+
start_date: str | None = None,
|
|
486
|
+
end_date: str | None = None,
|
|
487
|
+
source: str = "all",
|
|
488
|
+
query: str | None = None,
|
|
489
|
+
user_id: str | None = None,
|
|
490
|
+
client_id: str | None = None,
|
|
491
|
+
timezone: str = "UTC",
|
|
492
|
+
tool_error: str | None = None,
|
|
493
|
+
failed_only: bool = False,
|
|
494
|
+
include: list[str] | None = None,
|
|
495
|
+
limit: int = 50,
|
|
496
|
+
offset: int = 0,
|
|
497
|
+
) -> FindRunsResponse:
|
|
498
|
+
wanted = check_include(include or [], RUN_INCLUDES)
|
|
499
|
+
check_choice("invalid_source", source, RUN_SOURCES)
|
|
500
|
+
by_digest = tool_error is not None or failed_only
|
|
501
|
+
if by_digest:
|
|
502
|
+
wanted = wanted | {"metrics"}
|
|
503
|
+
if wanted and limit > FIND_RUNS_MAX:
|
|
504
|
+
raise fail(
|
|
505
|
+
"invalid_limit",
|
|
506
|
+
f"at most {FIND_RUNS_MAX} with include, tool_error or failed_only; got {limit}. "
|
|
507
|
+
"List the window without them (up to 5000), then get_runs for the ids in batches",
|
|
508
|
+
)
|
|
509
|
+
check_page(limit, offset, FIND_RUNS_MAX if wanted else FIND_RUNS_LIST_MAX)
|
|
510
|
+
since, until = window(start_date, end_date, zone_of(timezone))
|
|
511
|
+
budget = deadline()
|
|
512
|
+
record = await runs.agent(agent)
|
|
513
|
+
filters = {
|
|
514
|
+
"source": source,
|
|
515
|
+
"since": since,
|
|
516
|
+
"until": until,
|
|
517
|
+
"query": query,
|
|
518
|
+
"user_id": user_id,
|
|
519
|
+
"client_id": client_id,
|
|
520
|
+
}
|
|
521
|
+
if by_digest:
|
|
522
|
+
# Which runs failed, or errored in a tool, is known only from their
|
|
523
|
+
# trajectories: digest the window (as get_run_stats does), keep the
|
|
524
|
+
# matching runs, then page those.
|
|
525
|
+
def matches(found: RunDigest) -> bool:
|
|
526
|
+
if failed_only and not found.failures:
|
|
527
|
+
return False
|
|
528
|
+
return tool_error is None or any(
|
|
529
|
+
e.tool == tool_error for e in found.tool_errors_by_tool
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
listing = await runs.page(
|
|
533
|
+
record, limit=STATS_MAX_RUNS, offset=0, budget=budget, **filters
|
|
534
|
+
)
|
|
535
|
+
results = await runs.digests(record, [run.run_id for run in listing.runs], budget)
|
|
536
|
+
# A run that could not be read might match: it stays, carrying its
|
|
537
|
+
# note, rather than vanishing from the count.
|
|
538
|
+
hits = [
|
|
539
|
+
(run, result)
|
|
540
|
+
for run, result in zip(listing.runs, results, strict=True)
|
|
541
|
+
if (
|
|
542
|
+
result.status_code not in NOT_FOUND_STATUSES
|
|
543
|
+
if isinstance(result, HTTPException)
|
|
544
|
+
else matches(result[0])
|
|
545
|
+
)
|
|
546
|
+
]
|
|
547
|
+
rows = [
|
|
548
|
+
with_digest(run_row(run), result, wanted)
|
|
549
|
+
for run, result in hits[offset : offset + limit]
|
|
550
|
+
]
|
|
551
|
+
note = harness_note(listing) or unread_note(
|
|
552
|
+
results, truncated=listing.total > len(listing.runs)
|
|
553
|
+
)
|
|
554
|
+
meta = PagedMeta(**paged_meta(len(rows), len(hits), offset), note=note)
|
|
555
|
+
return FindRunsResponse(data=rows, meta=meta)
|
|
556
|
+
page = await runs.page(record, limit=limit, offset=offset, budget=budget, **filters)
|
|
557
|
+
rows = [run_row(run) for run in page.runs]
|
|
558
|
+
note = harness_note(page)
|
|
559
|
+
if wanted:
|
|
560
|
+
results = await runs.digests(record, [run.run_id for run in page.runs], budget)
|
|
561
|
+
rows = [with_digest(r, res, wanted) for r, res in zip(rows, results, strict=True)]
|
|
562
|
+
note = note or unread_note(results, truncated=False)
|
|
563
|
+
meta = PagedMeta(**paged_meta(len(rows), page.total, offset), note=note)
|
|
564
|
+
return FindRunsResponse(data=rows, meta=meta)
|
|
565
|
+
|
|
566
|
+
@mcp.tool(description=d.GET_RUNS, **HARNESS_READ_TOOL)
|
|
567
|
+
@expected_errors
|
|
568
|
+
async def get_runs(
|
|
569
|
+
agent: str, run_ids: list[str], include: list[str] | None = None
|
|
570
|
+
) -> GetRunsResponse:
|
|
571
|
+
check_ids(run_ids, GET_RUNS_MAX)
|
|
572
|
+
check_run_ids(run_ids)
|
|
573
|
+
wanted = check_include(["metrics"] if include is None else include, RUN_INCLUDES)
|
|
574
|
+
budget = deadline()
|
|
575
|
+
record = await runs.agent(agent)
|
|
576
|
+
page = await runs.page(record, limit=len(run_ids), offset=0, budget=budget, run_ids=run_ids)
|
|
577
|
+
listed = {run.run_id: run for run in page.runs}
|
|
578
|
+
results = await runs.digests(record, run_ids, budget)
|
|
579
|
+
rows = []
|
|
580
|
+
for run_id, result in zip(run_ids, results, strict=True):
|
|
581
|
+
run = listed.get(run_id)
|
|
582
|
+
row = run_row(run) if run else RunRow(id=run_id)
|
|
583
|
+
rows.append(with_digest(row, result, wanted))
|
|
584
|
+
unmatched = sum(1 for row in rows if row.note and row.note.code == "not_found")
|
|
585
|
+
failed = sum(1 for row in rows if row.error)
|
|
586
|
+
meta = BatchMeta(
|
|
587
|
+
count=len(rows),
|
|
588
|
+
matched=len(rows) - unmatched - failed,
|
|
589
|
+
unmatched=unmatched,
|
|
590
|
+
failed=failed or None,
|
|
591
|
+
)
|
|
592
|
+
return GetRunsResponse(data=rows, meta=meta)
|
|
593
|
+
|
|
594
|
+
@mcp.tool(description=d.GET_RUN_STATS, **HARNESS_READ_TOOL)
|
|
595
|
+
@expected_errors
|
|
596
|
+
async def get_run_stats(
|
|
597
|
+
agent: str,
|
|
598
|
+
start_date: str | None = None,
|
|
599
|
+
end_date: str | None = None,
|
|
600
|
+
group_by: str | list[str] = "none",
|
|
601
|
+
timezone: str = "UTC",
|
|
602
|
+
source: str = "all",
|
|
603
|
+
query: str | None = None,
|
|
604
|
+
user_id: str | None = None,
|
|
605
|
+
client_id: str | None = None,
|
|
606
|
+
max_runs: int = 500,
|
|
607
|
+
) -> GetRunStatsResponse:
|
|
608
|
+
groupings = [group_by] if isinstance(group_by, str) else list(dict.fromkeys(group_by))
|
|
609
|
+
bad = [g for g in groupings if g not in GROUP_BY_VALUES]
|
|
610
|
+
if bad or not groupings:
|
|
611
|
+
raise fail("invalid_group_by", f"{bad or group_by!r}; valid: {list(GROUP_BY_VALUES)}")
|
|
612
|
+
check_choice("invalid_source", source, RUN_SOURCES)
|
|
613
|
+
if not 1 <= max_runs <= STATS_MAX_RUNS:
|
|
614
|
+
raise fail(
|
|
615
|
+
"invalid_limit", f"max_runs must be between 1 and {STATS_MAX_RUNS}; got {max_runs}"
|
|
616
|
+
)
|
|
617
|
+
zone = zone_of(timezone)
|
|
618
|
+
since, until = window(start_date, end_date, zone)
|
|
619
|
+
budget = deadline()
|
|
620
|
+
record = await runs.agent(agent)
|
|
621
|
+
filters = {
|
|
622
|
+
"source": source,
|
|
623
|
+
"since": since,
|
|
624
|
+
"until": until,
|
|
625
|
+
"query": query,
|
|
626
|
+
"user_id": user_id,
|
|
627
|
+
"client_id": client_id,
|
|
628
|
+
}
|
|
629
|
+
page = await runs.page(record, limit=max_runs, offset=0, budget=budget, **filters)
|
|
630
|
+
listed, in_window = page.runs, page.total
|
|
631
|
+
results = await runs.digests(record, [run.run_id for run in listed], budget)
|
|
632
|
+
note = harness_note(page) or unread_note(results, truncated=False)
|
|
633
|
+
items = [
|
|
634
|
+
StatsInput(
|
|
635
|
+
digest=result[0],
|
|
636
|
+
turn_durations=[
|
|
637
|
+
t.duration_seconds for t in result[1] if t.duration_seconds is not None
|
|
638
|
+
],
|
|
639
|
+
source=run.source,
|
|
640
|
+
user_id=run.user_id,
|
|
641
|
+
created_at=run.created_at,
|
|
642
|
+
)
|
|
643
|
+
for run, result in zip(listed, results, strict=True)
|
|
644
|
+
if not isinstance(result, HTTPException)
|
|
645
|
+
]
|
|
646
|
+
rows = [
|
|
647
|
+
stats_row(s)
|
|
648
|
+
for g in groupings
|
|
649
|
+
for s in (group_stats(items, zone, g) if items else []) # type: ignore[arg-type]
|
|
650
|
+
]
|
|
651
|
+
meta = StatsMeta(
|
|
652
|
+
count=len(rows),
|
|
653
|
+
group_by=groupings,
|
|
654
|
+
timezone=timezone,
|
|
655
|
+
runs_considered=len(listed),
|
|
656
|
+
runs_in_window=in_window,
|
|
657
|
+
capped=in_window > len(listed),
|
|
658
|
+
unavailable=len(listed) - len(items),
|
|
659
|
+
note=note,
|
|
660
|
+
)
|
|
661
|
+
return GetRunStatsResponse(data=rows, meta=meta)
|
|
662
|
+
|
|
663
|
+
@mcp.tool(description=d.GET_TRAJECTORY, **HARNESS_READ_TOOL)
|
|
664
|
+
@expected_errors
|
|
665
|
+
async def get_trajectory(
|
|
666
|
+
agent: str,
|
|
667
|
+
run_id: str,
|
|
668
|
+
subagent: int | None = None,
|
|
669
|
+
max_chars: int = 2000,
|
|
670
|
+
limit: int = 20,
|
|
671
|
+
offset: int = 0,
|
|
672
|
+
) -> GetTrajectoryResponse:
|
|
673
|
+
check_page(limit, offset, STEP_PAGE_MAX)
|
|
674
|
+
if not 100 <= max_chars <= 50000:
|
|
675
|
+
raise fail("invalid_max_chars", "max_chars must be between 100 and 50000")
|
|
676
|
+
check_run_ids([run_id])
|
|
677
|
+
record = await runs.agent(agent)
|
|
678
|
+
root, _ = await runs.trajectory(record, run_id)
|
|
679
|
+
subs = root.subagent_trajectories or []
|
|
680
|
+
trajectory = root
|
|
681
|
+
if subagent is not None:
|
|
682
|
+
if not 0 <= subagent < len(subs):
|
|
683
|
+
raise fail("not_found", f"run has {len(subs)} subagent trajectories")
|
|
684
|
+
trajectory = subs[subagent]
|
|
685
|
+
steps = [clipped_step(s, max_chars) for s in trajectory.steps[offset : offset + limit]]
|
|
686
|
+
meta = TrajectoryMeta(
|
|
687
|
+
**paged_meta(len(steps), len(trajectory.steps), offset),
|
|
688
|
+
schema_version=root.schema_version,
|
|
689
|
+
session_id=trajectory.session_id,
|
|
690
|
+
trajectory_id=trajectory.trajectory_id,
|
|
691
|
+
agent=trajectory.agent.model_dump(exclude_none=True, exclude={"tool_definitions"})
|
|
692
|
+
if trajectory.agent
|
|
693
|
+
else None,
|
|
694
|
+
final_metrics=trajectory.final_metrics.model_dump(exclude_none=True)
|
|
695
|
+
if trajectory.final_metrics
|
|
696
|
+
else None,
|
|
697
|
+
subagents=[
|
|
698
|
+
SubagentRef(
|
|
699
|
+
index=i,
|
|
700
|
+
trajectory_id=sub.trajectory_id,
|
|
701
|
+
agent=sub.agent.name if sub.agent else None,
|
|
702
|
+
steps=len(sub.steps),
|
|
703
|
+
)
|
|
704
|
+
for i, sub in enumerate(subs)
|
|
705
|
+
],
|
|
706
|
+
)
|
|
707
|
+
return GetTrajectoryResponse(data=steps, meta=meta)
|
|
708
|
+
|
|
709
|
+
@mcp.tool(description=d.GET_RUN_FILES, **HARNESS_READ_TOOL)
|
|
710
|
+
@expected_errors
|
|
711
|
+
async def get_run_files(agent: str, run_id: str) -> GetRunFilesResponse:
|
|
712
|
+
check_run_ids([run_id])
|
|
713
|
+
listing = await runs_api.list_files(
|
|
714
|
+
check_agent_id(agent), run_id, state.runner_deps, state.agents
|
|
715
|
+
)
|
|
716
|
+
rows = [FileRow(**f) for f in listing["files"]]
|
|
717
|
+
return GetRunFilesResponse(data=rows, meta=PlainMeta(count=len(rows)))
|
|
718
|
+
|
|
719
|
+
@mcp.tool(description=d.DOWNLOAD_RUN_FILE, **HARNESS_READ_TOOL)
|
|
720
|
+
@expected_errors
|
|
721
|
+
async def download_run_file(
|
|
722
|
+
agent: str, run_id: str, path: str, max_chars: int = FILE_CHARS, offset: int = 0
|
|
723
|
+
) -> DownloadRunFileResponse:
|
|
724
|
+
if not 1 <= max_chars <= FILE_CHARS_MAX:
|
|
725
|
+
raise fail("invalid_max_chars", f"max_chars must be between 1 and {FILE_CHARS_MAX}")
|
|
726
|
+
if offset < 0:
|
|
727
|
+
raise fail("invalid_offset", f"offset must be 0 or greater; got {offset}")
|
|
728
|
+
check_run_ids([run_id])
|
|
729
|
+
response = await runs_api.get_file(
|
|
730
|
+
check_agent_id(agent), run_id, path, state.runner_deps, state.agents
|
|
731
|
+
)
|
|
732
|
+
body = b"".join(
|
|
733
|
+
[c.encode() if isinstance(c, str) else bytes(c) async for c in response.body_iterator]
|
|
734
|
+
)
|
|
735
|
+
try:
|
|
736
|
+
text = body.decode("utf-8")
|
|
737
|
+
except UnicodeDecodeError:
|
|
738
|
+
row = FileText(
|
|
739
|
+
path=path,
|
|
740
|
+
size=len(body),
|
|
741
|
+
note=Note(code="not_applicable", message="binary file; not returned as text"),
|
|
742
|
+
)
|
|
743
|
+
meta = PagedMeta(count=1, total=None, has_more=False)
|
|
744
|
+
return DownloadRunFileResponse(data=[row], meta=meta)
|
|
745
|
+
chunk = text[offset : offset + max_chars]
|
|
746
|
+
window_meta = paged_meta(len(chunk), len(text), offset)
|
|
747
|
+
meta = PagedMeta(**{**window_meta, "count": 1})
|
|
748
|
+
return DownloadRunFileResponse(
|
|
749
|
+
data=[FileText(path=path, size=len(body), text=chunk)], meta=meta
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
@mcp.tool(description=d.FIND_EXPERIMENTS, **READ_TOOL)
|
|
753
|
+
@expected_errors
|
|
754
|
+
async def find_experiments(
|
|
755
|
+
workspace: str | None = None,
|
|
756
|
+
status: str | None = None,
|
|
757
|
+
limit: int = 20,
|
|
758
|
+
offset: int = 0,
|
|
759
|
+
) -> FindExperimentsResponse:
|
|
760
|
+
check_page(limit, offset, EXPERIMENTS_MAX)
|
|
761
|
+
if status is not None:
|
|
762
|
+
check_choice("invalid_status", status, tuple(ExperimentStatus))
|
|
763
|
+
async with state.session_factory() as session:
|
|
764
|
+
found = await experiments_api.list_experiments(
|
|
765
|
+
session,
|
|
766
|
+
workspace=workspace,
|
|
767
|
+
status=ExperimentStatus(status) if status else None,
|
|
768
|
+
limit=limit + 1,
|
|
769
|
+
offset=offset,
|
|
770
|
+
)
|
|
771
|
+
rows = [ExperimentRow(**{k: v for k, v in e.items() if k != "config"}) for e in found]
|
|
772
|
+
meta = PagedMeta(**paged_meta(min(len(rows), limit), None, offset, len(rows) > limit))
|
|
773
|
+
rows = rows[:limit]
|
|
774
|
+
return FindExperimentsResponse(data=rows, meta=meta)
|
|
775
|
+
|
|
776
|
+
@mcp.tool(description=d.GET_EXPERIMENTS, **READ_TOOL)
|
|
777
|
+
@expected_errors
|
|
778
|
+
async def get_experiments(
|
|
779
|
+
experiment_ids: list[int], include: list[str] | None = None
|
|
780
|
+
) -> GetExperimentsResponse:
|
|
781
|
+
check_ids(experiment_ids, GET_EXPERIMENTS_MAX)
|
|
782
|
+
wanted = check_include(include or [], EXPERIMENT_INCLUDES)
|
|
783
|
+
rows = []
|
|
784
|
+
async with state.session_factory() as session:
|
|
785
|
+
for experiment_id in experiment_ids:
|
|
786
|
+
try:
|
|
787
|
+
exp = await db_experiments.get_experiment(session, experiment_id)
|
|
788
|
+
except UnknownExperiment:
|
|
789
|
+
rows.append(ExperimentRow(id=experiment_id, note=Note(code="not_found")))
|
|
790
|
+
continue
|
|
791
|
+
extra: dict[str, Any] = {
|
|
792
|
+
"progress": (await db_scorecard.progress(session, experiment_id)).model_dump()
|
|
793
|
+
}
|
|
794
|
+
if "scorecard" in wanted:
|
|
795
|
+
extra["scorecard"] = await experiments_api.scorecard(
|
|
796
|
+
experiment_id, state.store, None
|
|
797
|
+
)
|
|
798
|
+
rows.append(ExperimentRow(**exp.model_dump(exclude={"config"}), **extra))
|
|
799
|
+
unmatched = sum(1 for r in rows if r.note)
|
|
800
|
+
meta = BatchMeta(count=len(rows), matched=len(rows) - unmatched, unmatched=unmatched)
|
|
801
|
+
return GetExperimentsResponse(data=rows, meta=meta)
|
|
802
|
+
|
|
803
|
+
@mcp.tool(description=d.GET_EXPERIMENT_RUNS, **READ_TOOL)
|
|
804
|
+
@expected_errors
|
|
805
|
+
async def get_experiment_runs(
|
|
806
|
+
experiment_id: int, failed_only: bool = False, limit: int = 50, offset: int = 0
|
|
807
|
+
) -> GetExperimentRunsResponse:
|
|
808
|
+
check_page(limit, offset, FIND_RUNS_MAX)
|
|
809
|
+
exp = await state.store.get_experiment(experiment_id)
|
|
810
|
+
rows = []
|
|
811
|
+
for run in await state.store.run_details(experiment_id):
|
|
812
|
+
evaluations = [
|
|
813
|
+
Evaluation(
|
|
814
|
+
name=e.name,
|
|
815
|
+
kind=e.kind,
|
|
816
|
+
value=e.value,
|
|
817
|
+
label=e.label,
|
|
818
|
+
explanation=clip(e.explanation, 1000),
|
|
819
|
+
error=e.error,
|
|
820
|
+
)
|
|
821
|
+
for e in run.evaluations
|
|
822
|
+
]
|
|
823
|
+
failed = run.agent_run.error is not None or any(
|
|
824
|
+
e.kind == "score" and e.value is not None and e.value < 1 for e in evaluations
|
|
825
|
+
)
|
|
826
|
+
if failed_only and not failed:
|
|
827
|
+
continue
|
|
828
|
+
rows.append(
|
|
829
|
+
ExperimentRunRow(
|
|
830
|
+
id=run.agent_run.harness_run_id,
|
|
831
|
+
case_id=run.case_id,
|
|
832
|
+
repetition=run.repetition,
|
|
833
|
+
status=run.agent_run.status,
|
|
834
|
+
error=run.agent_run.error,
|
|
835
|
+
answer=clip(run.agent_run.answer, 2000),
|
|
836
|
+
started_at=run.agent_run.started_at,
|
|
837
|
+
ended_at=run.agent_run.ended_at,
|
|
838
|
+
metrics=run.agent_run.metrics,
|
|
839
|
+
evaluations=evaluations,
|
|
840
|
+
)
|
|
841
|
+
)
|
|
842
|
+
page = rows[offset : offset + limit]
|
|
843
|
+
meta = ExperimentRunsMeta(**paged_meta(len(page), len(rows), offset), agent=exp.agent)
|
|
844
|
+
return GetExperimentRunsResponse(data=page, meta=meta)
|