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/runs.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import zipfile
|
|
5
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
6
|
+
from contextlib import AsyncExitStack
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from posixpath import basename
|
|
9
|
+
from typing import IO, Annotated, Literal, cast
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
13
|
+
from pydantic import BaseModel, computed_field
|
|
14
|
+
from pydantic_core import from_json
|
|
15
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
16
|
+
from starlette.responses import StreamingResponse
|
|
17
|
+
|
|
18
|
+
from evalkit.api.deps import (
|
|
19
|
+
AgentStoreDep,
|
|
20
|
+
RunnerDepsDep,
|
|
21
|
+
SessionDep,
|
|
22
|
+
SessionReaderDep,
|
|
23
|
+
SpanStoreDep,
|
|
24
|
+
)
|
|
25
|
+
from evalkit.api.download import content_disposition
|
|
26
|
+
from evalkit.api.experiments import slug_or_400
|
|
27
|
+
from evalkit.contracts.atif import Trajectory, validate_trajectory
|
|
28
|
+
from evalkit.contracts.harness import FileEntry, FileList, RunListItem
|
|
29
|
+
from evalkit.core.agents import AgentDefinition, explicit, server_may_drive
|
|
30
|
+
from evalkit.core.blob import artifact_key, workspace_key
|
|
31
|
+
from evalkit.core.costmap import load_cost_map
|
|
32
|
+
from evalkit.core.harness import HarnessClient, HarnessFile
|
|
33
|
+
from evalkit.core.paths import is_safe_path, is_safe_run_id
|
|
34
|
+
from evalkit.core.pull import SCAN_NOTE, list_harness_runs
|
|
35
|
+
from evalkit.core.runner import RunnerDeps, case_title
|
|
36
|
+
from evalkit.core.trajectory import backfill_step_costs
|
|
37
|
+
from evalkit.db.agents import AgentStore
|
|
38
|
+
from evalkit.db.runs import (
|
|
39
|
+
AgentRunItem,
|
|
40
|
+
RunSource,
|
|
41
|
+
agent_run,
|
|
42
|
+
list_agent_runs,
|
|
43
|
+
runs_by_harness_ids,
|
|
44
|
+
)
|
|
45
|
+
from evalkit.db.sessions import DbSessionReader
|
|
46
|
+
from evalkit.db.spans import SessionSummary, SpanStore
|
|
47
|
+
|
|
48
|
+
router = APIRouter(prefix="/api/agents", tags=["agents"])
|
|
49
|
+
|
|
50
|
+
HARNESS_UNREACHABLE = "harness unreachable"
|
|
51
|
+
MAX_PAGE = 200
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BrowserRun(RunListItem):
|
|
55
|
+
status: str | None = None
|
|
56
|
+
experiment_id: int | None = None
|
|
57
|
+
experiment_number: int | None = None
|
|
58
|
+
case_id: str | None = None
|
|
59
|
+
stored: bool = False
|
|
60
|
+
|
|
61
|
+
@computed_field # type: ignore[prop-decorator]
|
|
62
|
+
@property
|
|
63
|
+
def source(self) -> Literal["app", "eval"]:
|
|
64
|
+
return "eval" if self.experiment_id is not None else "app"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RunPage(BaseModel):
|
|
68
|
+
runs: list[BrowserRun]
|
|
69
|
+
total: int
|
|
70
|
+
harness_note: str | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def harness_client(deps: RunnerDeps, agent: AgentDefinition) -> HarnessClient:
|
|
74
|
+
assert deps.agent_keys is not None
|
|
75
|
+
return HarnessClient(
|
|
76
|
+
agent, await deps.agent_keys.agent_key(agent), transport=deps.harness_transport
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def agent_or_404(agents: AgentStore, agent: str) -> AgentDefinition:
|
|
81
|
+
return await agents.get(explicit(slug_or_400(agent)))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _sdk_case(item: SessionSummary) -> tuple[str | None, str | None]:
|
|
85
|
+
if item.sdk_root is None:
|
|
86
|
+
return None, None
|
|
87
|
+
case_id = item.sdk_root.get("evalkit.case_id")
|
|
88
|
+
raw_input = item.sdk_root.get("input.value")
|
|
89
|
+
title = case_title(from_json(raw_input, allow_partial=True)) if raw_input else None
|
|
90
|
+
return (str(case_id) if case_id is not None else None), title
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _stored(item: AgentRunItem) -> BrowserRun:
|
|
94
|
+
return BrowserRun(
|
|
95
|
+
run_id=item.harness_run_id,
|
|
96
|
+
title=item.title,
|
|
97
|
+
user_id=item.created_by,
|
|
98
|
+
created_at=item.started_at,
|
|
99
|
+
status=item.status.value,
|
|
100
|
+
experiment_id=item.experiment_id,
|
|
101
|
+
experiment_number=item.experiment_number,
|
|
102
|
+
case_id=item.case_id,
|
|
103
|
+
stored=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _merge(stored: list[BrowserRun], harness: list[RunListItem], limit: int) -> list[BrowserRun]:
|
|
108
|
+
seen = {run.run_id for run in stored}
|
|
109
|
+
extra = [BrowserRun(**run.model_dump()) for run in harness if run.run_id not in seen]
|
|
110
|
+
oldest = datetime.min.replace(tzinfo=UTC)
|
|
111
|
+
merged = sorted([*stored, *extra], key=lambda run: run.created_at or oldest, reverse=True)
|
|
112
|
+
return merged[:limit]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def resolve_runs(
|
|
116
|
+
record: AgentDefinition,
|
|
117
|
+
deps: RunnerDeps,
|
|
118
|
+
spans: SpanStore,
|
|
119
|
+
session: AsyncSession,
|
|
120
|
+
*,
|
|
121
|
+
source: RunSource = "all",
|
|
122
|
+
since: datetime | None = None,
|
|
123
|
+
until: datetime | None = None,
|
|
124
|
+
query: str | None = None,
|
|
125
|
+
user_id: str | None = None,
|
|
126
|
+
client_id: str | None = None,
|
|
127
|
+
limit: int = 50,
|
|
128
|
+
offset: int = 0,
|
|
129
|
+
no_session: bool = False,
|
|
130
|
+
run_ids: list[str] | None = None,
|
|
131
|
+
) -> RunPage:
|
|
132
|
+
if run_ids is not None:
|
|
133
|
+
known = await runs_by_harness_ids(session, record.id, run_ids)
|
|
134
|
+
rows = [_stored(known[run_id]) for run_id in run_ids if run_id in known]
|
|
135
|
+
return RunPage(runs=rows, total=len(rows))
|
|
136
|
+
if record.is_otel:
|
|
137
|
+
if no_session:
|
|
138
|
+
items, total = await spans.list_unsessioned(record.id, limit=limit, offset=offset)
|
|
139
|
+
else:
|
|
140
|
+
items, total = await spans.list_sessions(
|
|
141
|
+
record.id,
|
|
142
|
+
query=query,
|
|
143
|
+
limit=limit,
|
|
144
|
+
offset=offset,
|
|
145
|
+
since=since,
|
|
146
|
+
until=until,
|
|
147
|
+
linked=source,
|
|
148
|
+
)
|
|
149
|
+
linked = await runs_by_harness_ids(
|
|
150
|
+
session, record.id, [i.session_id for i in items if i.session_id]
|
|
151
|
+
)
|
|
152
|
+
sessions: list[BrowserRun] = []
|
|
153
|
+
for i in items:
|
|
154
|
+
run = linked.get(i.session_id) if i.session_id else None
|
|
155
|
+
sdk_case_id, sdk_title = _sdk_case(i)
|
|
156
|
+
sessions.append(
|
|
157
|
+
BrowserRun(
|
|
158
|
+
run_id=i.session_id or i.trace_id,
|
|
159
|
+
title=(run.title if run else None) or sdk_title or i.title,
|
|
160
|
+
created_at=i.created_at,
|
|
161
|
+
user_id=run.created_by if run else None,
|
|
162
|
+
status=run.status.value if run else None,
|
|
163
|
+
experiment_id=run.experiment_id if run else None,
|
|
164
|
+
experiment_number=run.experiment_number if run else None,
|
|
165
|
+
case_id=(run.case_id if run else None) or sdk_case_id,
|
|
166
|
+
stored=run is not None,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
return RunPage(runs=sessions, total=total)
|
|
170
|
+
stored_items, stored_total = await list_agent_runs(
|
|
171
|
+
session,
|
|
172
|
+
record.id,
|
|
173
|
+
query=query,
|
|
174
|
+
limit=limit + offset,
|
|
175
|
+
created_by=user_id,
|
|
176
|
+
since=since,
|
|
177
|
+
until=until,
|
|
178
|
+
source=source,
|
|
179
|
+
)
|
|
180
|
+
stored = [_stored(item) for item in stored_items]
|
|
181
|
+
if source == "eval" or not server_may_drive(record):
|
|
182
|
+
return RunPage(runs=stored[offset : offset + limit], total=stored_total)
|
|
183
|
+
client = await harness_client(deps, record)
|
|
184
|
+
note: str | None = None
|
|
185
|
+
harness: list[RunListItem] = []
|
|
186
|
+
harness_total = 0
|
|
187
|
+
|
|
188
|
+
async def evals(run_ids: list[str]) -> set[str]:
|
|
189
|
+
known = await runs_by_harness_ids(session, record.id, run_ids)
|
|
190
|
+
return {run_id for run_id, item in known.items() if item.experiment_id is not None}
|
|
191
|
+
|
|
192
|
+
# Spec "Runs browser": an unreachable harness still lists the stored runs, with a note.
|
|
193
|
+
try:
|
|
194
|
+
listed = await list_harness_runs(
|
|
195
|
+
client,
|
|
196
|
+
wanted=limit + offset,
|
|
197
|
+
since=since,
|
|
198
|
+
until=until,
|
|
199
|
+
query=query,
|
|
200
|
+
user_id=user_id,
|
|
201
|
+
client_id=client_id,
|
|
202
|
+
exclude=evals if source == "app" else None,
|
|
203
|
+
)
|
|
204
|
+
harness, harness_total = listed.runs, listed.total
|
|
205
|
+
if listed.capped:
|
|
206
|
+
note = SCAN_NOTE
|
|
207
|
+
except httpx.HTTPError:
|
|
208
|
+
note = HARNESS_UNREACHABLE
|
|
209
|
+
finally:
|
|
210
|
+
await client.aclose()
|
|
211
|
+
harness_ids = {run.run_id for run in harness}
|
|
212
|
+
if client_id is not None:
|
|
213
|
+
stored = [run for run in stored if run.run_id in harness_ids]
|
|
214
|
+
stored_total = len(stored)
|
|
215
|
+
overlap = sum(1 for run in stored if run.run_id in harness_ids)
|
|
216
|
+
merged = _merge(stored, harness, limit + offset)[offset:]
|
|
217
|
+
return RunPage(
|
|
218
|
+
runs=merged,
|
|
219
|
+
total=max(stored_total + harness_total - overlap, len(merged) + offset),
|
|
220
|
+
harness_note=note,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@router.get("/{agent}/runs")
|
|
225
|
+
async def list_runs(
|
|
226
|
+
agent: str,
|
|
227
|
+
deps: RunnerDepsDep,
|
|
228
|
+
agents: AgentStoreDep,
|
|
229
|
+
spans: SpanStoreDep,
|
|
230
|
+
session: SessionDep,
|
|
231
|
+
query: str | None = None,
|
|
232
|
+
user_id: str | None = None,
|
|
233
|
+
client_id: str | None = None,
|
|
234
|
+
limit: int = 50,
|
|
235
|
+
offset: int = 0,
|
|
236
|
+
no_session: bool = False,
|
|
237
|
+
source: RunSource = "all",
|
|
238
|
+
since: datetime | None = None,
|
|
239
|
+
until: datetime | None = None,
|
|
240
|
+
run_id: Annotated[list[str] | None, Query()] = None,
|
|
241
|
+
) -> dict:
|
|
242
|
+
page = await resolve_runs(
|
|
243
|
+
await agent_or_404(agents, agent),
|
|
244
|
+
deps,
|
|
245
|
+
spans,
|
|
246
|
+
session,
|
|
247
|
+
source=source,
|
|
248
|
+
since=since,
|
|
249
|
+
until=until,
|
|
250
|
+
query=query,
|
|
251
|
+
user_id=user_id,
|
|
252
|
+
client_id=client_id,
|
|
253
|
+
limit=min(limit, MAX_PAGE),
|
|
254
|
+
offset=offset,
|
|
255
|
+
no_session=no_session,
|
|
256
|
+
run_ids=run_id,
|
|
257
|
+
)
|
|
258
|
+
return page.model_dump(
|
|
259
|
+
mode="json", exclude={"harness_note"} if page.harness_note is None else None
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
async def read_trajectory(
|
|
264
|
+
record: AgentDefinition,
|
|
265
|
+
run_id: str,
|
|
266
|
+
deps: RunnerDeps,
|
|
267
|
+
sessions: DbSessionReader,
|
|
268
|
+
trajectory_key: str | None,
|
|
269
|
+
client: HarnessClient | None,
|
|
270
|
+
) -> Trajectory:
|
|
271
|
+
if trajectory_key:
|
|
272
|
+
return validate_trajectory(json.loads(await deps.blob_store.get_object(trajectory_key)))
|
|
273
|
+
if record.is_otel:
|
|
274
|
+
result = await sessions.read(record.ref, run_id)
|
|
275
|
+
if result is None or result.trajectory is None:
|
|
276
|
+
raise HTTPException(status_code=404, detail="no spans for this session")
|
|
277
|
+
return validate_trajectory(result.trajectory)
|
|
278
|
+
if client is not None:
|
|
279
|
+
try:
|
|
280
|
+
return await client.trajectory(run_id)
|
|
281
|
+
except httpx.HTTPStatusError as exc:
|
|
282
|
+
raise HTTPException(
|
|
283
|
+
status_code=exc.response.status_code, detail="harness error"
|
|
284
|
+
) from exc
|
|
285
|
+
raise HTTPException(status_code=404, detail="run has no stored trajectory")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def fetch_trajectory(
|
|
289
|
+
record: AgentDefinition,
|
|
290
|
+
run_id: str,
|
|
291
|
+
deps: RunnerDeps,
|
|
292
|
+
sessions: DbSessionReader,
|
|
293
|
+
session: AsyncSession,
|
|
294
|
+
) -> Trajectory:
|
|
295
|
+
stored = await agent_run(session, record.id, run_id)
|
|
296
|
+
key = stored.trajectory_key if stored is not None else None
|
|
297
|
+
client = await harness_client(deps, record) if not key and server_may_drive(record) else None
|
|
298
|
+
try:
|
|
299
|
+
return await read_trajectory(record, run_id, deps, sessions, key, client)
|
|
300
|
+
finally:
|
|
301
|
+
if client is not None:
|
|
302
|
+
await client.aclose()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@router.get("/{agent}/runs/{run_id}/trajectory")
|
|
306
|
+
async def live_trajectory(
|
|
307
|
+
agent: str,
|
|
308
|
+
run_id: str,
|
|
309
|
+
deps: RunnerDepsDep,
|
|
310
|
+
agents: AgentStoreDep,
|
|
311
|
+
sessions: SessionReaderDep,
|
|
312
|
+
session: SessionDep,
|
|
313
|
+
) -> dict:
|
|
314
|
+
_run_id_or_400(run_id)
|
|
315
|
+
record = await agent_or_404(agents, agent)
|
|
316
|
+
trajectory = await fetch_trajectory(record, run_id, deps, sessions, session)
|
|
317
|
+
# Price any step the harness left uncosted so the cost view matches the
|
|
318
|
+
# scorecard gauge (see backfill_step_costs).
|
|
319
|
+
backfill_step_costs(trajectory, load_cost_map(deps.cfg.cost_map))
|
|
320
|
+
return trajectory.model_dump(exclude_none=True)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
NOT_ON_HARNESS = "not found on the harness: wrong path, or the harness no longer has it"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _run_id_or_400(run_id: str) -> str:
|
|
327
|
+
if not is_safe_run_id(run_id):
|
|
328
|
+
raise HTTPException(status_code=400, detail="run id must be one plain path segment")
|
|
329
|
+
return run_id
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _path_or_400(path: str) -> str:
|
|
333
|
+
if not is_safe_path(path):
|
|
334
|
+
raise HTTPException(status_code=400, detail="path must have no empty, '.' or '..' segments")
|
|
335
|
+
return path.lstrip("/")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _harness_status(exc: httpx.HTTPStatusError) -> HTTPException:
|
|
339
|
+
if exc.response.status_code == 404:
|
|
340
|
+
return HTTPException(status_code=404, detail=NOT_ON_HARNESS)
|
|
341
|
+
return HTTPException(status_code=exc.response.status_code, detail="harness error")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
async def _stored_files(deps: RunnerDeps, record: AgentDefinition, run_id: str) -> dict[str, int]:
|
|
345
|
+
prefix = artifact_key(record.id, run_id, "")
|
|
346
|
+
return {
|
|
347
|
+
key.removeprefix(prefix): size for key, size in await deps.blob_store.list_objects(prefix)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
async def _snapshot(deps: RunnerDeps, record: AgentDefinition, run_id: str) -> dict[str, FileEntry]:
|
|
352
|
+
key = workspace_key(record.id, run_id)
|
|
353
|
+
if not await deps.blob_store.object_exists(key):
|
|
354
|
+
return {}
|
|
355
|
+
tree = FileList.model_validate_json(await deps.blob_store.get_object(key))
|
|
356
|
+
return {entry.path: entry for entry in tree.files}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async def _listing(
|
|
360
|
+
deps: RunnerDeps, record: AgentDefinition, run_id: str
|
|
361
|
+
) -> tuple[dict[str, FileEntry], set[str]]:
|
|
362
|
+
stored = await _stored_files(deps, record, run_id)
|
|
363
|
+
snapshot = await _snapshot(deps, record, run_id)
|
|
364
|
+
if snapshot or stored or not server_may_drive(record):
|
|
365
|
+
kept = {path: FileEntry(path=path, size=size) for path, size in stored.items()}
|
|
366
|
+
return {**kept, **snapshot}, set(stored)
|
|
367
|
+
client = await harness_client(deps, record)
|
|
368
|
+
try:
|
|
369
|
+
files = await client.list_files(run_id)
|
|
370
|
+
except httpx.HTTPStatusError as exc:
|
|
371
|
+
raise _harness_status(exc) from exc
|
|
372
|
+
finally:
|
|
373
|
+
await client.aclose()
|
|
374
|
+
return {entry.path: entry for entry in files.files}, set()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@router.get("/{agent}/runs/{run_id}/files")
|
|
378
|
+
async def list_files(agent: str, run_id: str, deps: RunnerDepsDep, agents: AgentStoreDep) -> dict:
|
|
379
|
+
_run_id_or_400(run_id)
|
|
380
|
+
record = await agent_or_404(agents, agent)
|
|
381
|
+
listing, stored = await _listing(deps, record, run_id)
|
|
382
|
+
return {
|
|
383
|
+
"files": [
|
|
384
|
+
{**entry.model_dump(), "stored": path in stored}
|
|
385
|
+
for path, entry in sorted(listing.items())
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
LOGGER = logging.getLogger(__name__)
|
|
391
|
+
|
|
392
|
+
# A single oversized file is refused before the response starts; once headers
|
|
393
|
+
# are out an error can only truncate the zip.
|
|
394
|
+
MAX_FILE_BYTES = 512 * 1024 * 1024
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class _ChunkSink:
|
|
398
|
+
def __init__(self) -> None:
|
|
399
|
+
self.buffer = bytearray()
|
|
400
|
+
|
|
401
|
+
def write(self, data: bytes) -> int:
|
|
402
|
+
self.buffer.extend(data)
|
|
403
|
+
return len(data)
|
|
404
|
+
|
|
405
|
+
def flush(self) -> None:
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
def drain(self) -> bytes:
|
|
409
|
+
chunk = bytes(self.buffer)
|
|
410
|
+
self.buffer.clear()
|
|
411
|
+
return chunk
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# zipfile writes to the unseekable sink with data descriptors, so every entry
|
|
415
|
+
# is flushed to the client as soon as it is compressed and the archive never
|
|
416
|
+
# sits in memory as a whole. A fetch failure after the headers are out cannot
|
|
417
|
+
# become a status code; re-raising drops the connection so the browser shows
|
|
418
|
+
# a failed download rather than a quietly smaller zip.
|
|
419
|
+
async def _zip_stream(
|
|
420
|
+
entries: list[tuple[str, str]],
|
|
421
|
+
fetch: Callable[[str], Awaitable[AsyncIterator[bytes]]],
|
|
422
|
+
close: Callable[[], Awaitable[None]],
|
|
423
|
+
) -> AsyncIterator[bytes]:
|
|
424
|
+
sink = _ChunkSink()
|
|
425
|
+
try:
|
|
426
|
+
with zipfile.ZipFile(cast(IO[bytes], sink), "w", zipfile.ZIP_DEFLATED) as archive:
|
|
427
|
+
for arcname, path in entries:
|
|
428
|
+
try:
|
|
429
|
+
chunks = await fetch(path)
|
|
430
|
+
except httpx.HTTPStatusError as exc:
|
|
431
|
+
LOGGER.warning(
|
|
432
|
+
"zip aborted: harness returned %s for %s", exc.response.status_code, path
|
|
433
|
+
)
|
|
434
|
+
raise
|
|
435
|
+
with archive.open(arcname, "w", force_zip64=True) as entry:
|
|
436
|
+
async for chunk in chunks:
|
|
437
|
+
await asyncio.to_thread(entry.write, chunk)
|
|
438
|
+
yield sink.drain()
|
|
439
|
+
yield sink.drain()
|
|
440
|
+
yield sink.drain()
|
|
441
|
+
finally:
|
|
442
|
+
await close()
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _zip_entries(
|
|
446
|
+
listing: dict[str, int], folder: str, fallback_name: str
|
|
447
|
+
) -> tuple[str, list[tuple[str, str]]]:
|
|
448
|
+
prefix = f"{folder}/" if folder else ""
|
|
449
|
+
root = basename(folder) if folder else fallback_name
|
|
450
|
+
selected = sorted(p for p in listing if p.lstrip("/").startswith(prefix) and is_safe_path(p))
|
|
451
|
+
if not selected:
|
|
452
|
+
raise HTTPException(status_code=404, detail=f"no files under {folder or '/'}")
|
|
453
|
+
largest = max(selected, key=lambda p: listing[p])
|
|
454
|
+
if listing[largest] > MAX_FILE_BYTES:
|
|
455
|
+
raise HTTPException(
|
|
456
|
+
status_code=413,
|
|
457
|
+
detail=f"{largest} is {listing[largest]} bytes; zip entries stop at {MAX_FILE_BYTES}",
|
|
458
|
+
)
|
|
459
|
+
return root, [(f"{root}/{p.lstrip('/')[len(prefix) :]}", p) for p in selected]
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _zip_response(
|
|
463
|
+
root: str,
|
|
464
|
+
entries: list[tuple[str, str]],
|
|
465
|
+
fetch: Callable[[str], Awaitable[AsyncIterator[bytes]]],
|
|
466
|
+
close: Callable[[], Awaitable[None]],
|
|
467
|
+
) -> StreamingResponse:
|
|
468
|
+
return StreamingResponse(
|
|
469
|
+
_zip_stream(entries, fetch, close),
|
|
470
|
+
media_type="application/zip",
|
|
471
|
+
headers=content_disposition(f"{root}.zip"),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
async def _noop() -> None:
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@router.get("/{agent}/runs/{run_id}/zip")
|
|
480
|
+
async def zip_files(
|
|
481
|
+
agent: str, run_id: str, deps: RunnerDepsDep, agents: AgentStoreDep, path: str = ""
|
|
482
|
+
) -> StreamingResponse:
|
|
483
|
+
_run_id_or_400(run_id)
|
|
484
|
+
folder = path.strip("/")
|
|
485
|
+
if folder:
|
|
486
|
+
_path_or_400(folder)
|
|
487
|
+
agent_def = await agent_or_404(agents, agent)
|
|
488
|
+
listing, stored = await _listing(deps, agent_def, run_id)
|
|
489
|
+
drivable = server_may_drive(agent_def)
|
|
490
|
+
if not drivable:
|
|
491
|
+
listing = {p: entry for p, entry in listing.items() if p in stored}
|
|
492
|
+
sizes = {p: entry.size for p, entry in listing.items()}
|
|
493
|
+
root, entries = _zip_entries(sizes, folder, run_id)
|
|
494
|
+
prefix = artifact_key(agent_def.id, run_id, "")
|
|
495
|
+
|
|
496
|
+
async def stored_chunks(p: str) -> AsyncIterator[bytes]:
|
|
497
|
+
return (await deps.blob_store.get_stream(f"{prefix}{p}")).chunks
|
|
498
|
+
|
|
499
|
+
if all(p in stored for _, p in entries):
|
|
500
|
+
return _zip_response(root, entries, stored_chunks, _noop)
|
|
501
|
+
client = await harness_client(deps, agent_def)
|
|
502
|
+
|
|
503
|
+
async def fetch(p: str) -> AsyncIterator[bytes]:
|
|
504
|
+
if p in stored:
|
|
505
|
+
return await stored_chunks(p)
|
|
506
|
+
return (await client.stream_file(run_id, p)).chunks
|
|
507
|
+
|
|
508
|
+
return _zip_response(root, entries, fetch, client.aclose)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
async def _closing(file: HarnessFile, client: HarnessClient) -> AsyncIterator[bytes]:
|
|
512
|
+
try:
|
|
513
|
+
async for chunk in file.chunks:
|
|
514
|
+
yield chunk
|
|
515
|
+
finally:
|
|
516
|
+
await client.aclose()
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@router.get("/{agent}/runs/{run_id}/files/{path:path}")
|
|
520
|
+
async def get_file(
|
|
521
|
+
agent: str,
|
|
522
|
+
run_id: str,
|
|
523
|
+
path: str,
|
|
524
|
+
deps: RunnerDepsDep,
|
|
525
|
+
agents: AgentStoreDep,
|
|
526
|
+
download: bool = False,
|
|
527
|
+
) -> StreamingResponse:
|
|
528
|
+
_run_id_or_400(run_id)
|
|
529
|
+
path = _path_or_400(path)
|
|
530
|
+
headers = content_disposition(basename(path)) if download else None
|
|
531
|
+
record = await agent_or_404(agents, agent)
|
|
532
|
+
key = artifact_key(record.id, run_id, path)
|
|
533
|
+
if await deps.blob_store.object_exists(key):
|
|
534
|
+
blob = await deps.blob_store.get_stream(key)
|
|
535
|
+
return StreamingResponse(
|
|
536
|
+
blob.chunks,
|
|
537
|
+
media_type="application/octet-stream",
|
|
538
|
+
headers={"Content-Length": str(blob.size), **(headers or {})},
|
|
539
|
+
)
|
|
540
|
+
if server_may_drive(record):
|
|
541
|
+
client = await harness_client(deps, record)
|
|
542
|
+
async with AsyncExitStack() as cleanup:
|
|
543
|
+
cleanup.push_async_callback(client.aclose)
|
|
544
|
+
try:
|
|
545
|
+
file = await client.stream_file(run_id, path)
|
|
546
|
+
except httpx.HTTPStatusError as exc:
|
|
547
|
+
raise _harness_status(exc) from exc
|
|
548
|
+
cleanup.pop_all()
|
|
549
|
+
length = {"Content-Length": str(file.size)} if file.size is not None else {}
|
|
550
|
+
return StreamingResponse(
|
|
551
|
+
_closing(file, client),
|
|
552
|
+
media_type="application/octet-stream",
|
|
553
|
+
headers={**length, **(headers or {})},
|
|
554
|
+
)
|
|
555
|
+
raise HTTPException(status_code=404, detail="no such file")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import replace
|
|
3
|
+
|
|
4
|
+
from fastapi import Request
|
|
5
|
+
|
|
6
|
+
from evalkit.config import Config
|
|
7
|
+
from evalkit.core.agents import AgentDefinition
|
|
8
|
+
from evalkit.core.endpoints import check_endpoint
|
|
9
|
+
from evalkit.core.runner import RunnerDeps
|
|
10
|
+
from evalkit.db.keys import KeyLoader, StoredKeys
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def server_keys(
|
|
16
|
+
request: Request, agent: AgentDefinition, judge_base_url: str | None
|
|
17
|
+
) -> tuple[StoredKeys, list[str]]:
|
|
18
|
+
cfg: Config = request.app.state.cfg
|
|
19
|
+
deps: RunnerDeps = request.app.state.runner_deps
|
|
20
|
+
loader: KeyLoader = request.app.state.keys
|
|
21
|
+
for url in (agent.harness_url, judge_base_url):
|
|
22
|
+
if url is not None:
|
|
23
|
+
await check_endpoint(url, allow_private=cfg.allow_private_endpoints)
|
|
24
|
+
keys = await loader.load(agent, judge_base_url)
|
|
25
|
+
warnings = []
|
|
26
|
+
if agent.harness_url is not None and keys.key_for("agent", agent.ref) is None:
|
|
27
|
+
warnings.append(
|
|
28
|
+
f"agent {agent.ref} has no API key; calling its harness without x-api-key. "
|
|
29
|
+
f"Set one with evalkit agents edit {agent.ref} --api-key-env NAME"
|
|
30
|
+
)
|
|
31
|
+
for evaluator in (await deps.evaluators.in_workspace(agent.workspace)).values():
|
|
32
|
+
if keys.key_for("evaluator", evaluator.ref) is None:
|
|
33
|
+
warnings.append(
|
|
34
|
+
f"evaluator {evaluator.ref} has no API key; calling it without x-api-key"
|
|
35
|
+
)
|
|
36
|
+
if judge_base_url is not None and keys.require_judge_key(judge_base_url) is None:
|
|
37
|
+
warnings.append(f"LLM provider {judge_base_url} has no API key; judging without one")
|
|
38
|
+
for warning in warnings:
|
|
39
|
+
LOGGER.warning("%s", warning)
|
|
40
|
+
return keys, warnings
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def with_keys(deps: RunnerDeps, keys: StoredKeys) -> RunnerDeps:
|
|
44
|
+
return replace(deps, credentials=keys)
|
evalkit/api/tokens.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from evalkit.api.access import ActorDep, ensure_active, require, workspace_ref
|
|
5
|
+
from evalkit.api.auth import CurrentUser
|
|
6
|
+
from evalkit.api.deps import AgentStoreDep, AgentTokensDep, SessionDep
|
|
7
|
+
from evalkit.core.access import can_write
|
|
8
|
+
from evalkit.core.agents import explicit
|
|
9
|
+
from evalkit.core.workspace import parse_reference
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/api/tokens", tags=["tokens"])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TokenRequest(BaseModel):
|
|
15
|
+
agent: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post("")
|
|
19
|
+
async def create_token(
|
|
20
|
+
body: TokenRequest,
|
|
21
|
+
agents: AgentStoreDep,
|
|
22
|
+
tokens: AgentTokensDep,
|
|
23
|
+
session: SessionDep,
|
|
24
|
+
actor: ActorDep,
|
|
25
|
+
user: CurrentUser,
|
|
26
|
+
) -> dict:
|
|
27
|
+
try:
|
|
28
|
+
ref = parse_reference(body.agent, user.username)
|
|
29
|
+
except ValueError as exc:
|
|
30
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
31
|
+
record = await agents.get(explicit(ref))
|
|
32
|
+
ws = await workspace_ref(session, record.workspace)
|
|
33
|
+
require(can_write(actor, ws), f"you cannot mint tokens for {record.ref}")
|
|
34
|
+
ensure_active(ws)
|
|
35
|
+
if record.archived:
|
|
36
|
+
raise HTTPException(status_code=409, detail=f"agent {record.ref} is archived")
|
|
37
|
+
token, info = await tokens.create(agent_id=record.id, user_id=user.id)
|
|
38
|
+
return {"id": info.id, "token": token, "agent": info.agent}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.get("")
|
|
42
|
+
async def list_tokens(tokens: AgentTokensDep, user: CurrentUser) -> list[dict]:
|
|
43
|
+
rows = await tokens.list(user_id=None if user.is_admin else user.id)
|
|
44
|
+
return [r.model_dump(mode="json") for r in rows]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.delete("/{token_id}")
|
|
48
|
+
async def revoke_token(token_id: int, tokens: AgentTokensDep, user: CurrentUser) -> dict:
|
|
49
|
+
if not await tokens.revoke(token_id, user_id=None if user.is_admin else user.id):
|
|
50
|
+
raise HTTPException(status_code=404, detail="unknown token")
|
|
51
|
+
return {"ok": True}
|