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/fixtures.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import mimetypes
|
|
3
|
+
import re
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
from starlette.responses import Response, StreamingResponse
|
|
9
|
+
|
|
10
|
+
from evalkit.api.access import ActorDep, workspace_ref
|
|
11
|
+
from evalkit.api.deps import RunnerDepsDep, SessionDep
|
|
12
|
+
from evalkit.api.download import content_disposition
|
|
13
|
+
from evalkit.core.blob import fixture_blob_key
|
|
14
|
+
from evalkit.db.fixture_blobs import blob_size, known_blobs, record_blob, workspace_fixtures
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/fixtures", tags=["fixtures"])
|
|
17
|
+
|
|
18
|
+
FIXTURE_STAGING_PREFIX = "fixtures/staging/"
|
|
19
|
+
|
|
20
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _sha(value: str) -> str:
|
|
24
|
+
if not _SHA256.match(value):
|
|
25
|
+
raise HTTPException(status_code=400, detail=f"{value!r} is not a lowercase sha256 digest")
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("")
|
|
30
|
+
async def list_fixtures(
|
|
31
|
+
session: SessionDep,
|
|
32
|
+
actor: ActorDep,
|
|
33
|
+
workspace: str | None = None,
|
|
34
|
+
q: str = "",
|
|
35
|
+
limit: int = 50,
|
|
36
|
+
offset: int = 0,
|
|
37
|
+
) -> dict:
|
|
38
|
+
ws = await workspace_ref(session, workspace or actor.username)
|
|
39
|
+
page = await workspace_fixtures(
|
|
40
|
+
session, ws.id, text=q or None, limit=max(1, min(limit, 200)), offset=max(0, offset)
|
|
41
|
+
)
|
|
42
|
+
return page.model_dump(mode="json")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _known(session: SessionDep, sha256: str) -> None:
|
|
46
|
+
if not await known_blobs(session, [_sha(sha256)]):
|
|
47
|
+
raise HTTPException(status_code=404, detail=f"no fixture with sha256 {sha256}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.head("/sha256/{sha256}")
|
|
51
|
+
async def head(sha256: str, session: SessionDep) -> Response:
|
|
52
|
+
await _known(session, sha256)
|
|
53
|
+
return Response(status_code=200)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get("/sha256/{sha256}")
|
|
57
|
+
async def download(
|
|
58
|
+
sha256: str, deps: RunnerDepsDep, session: SessionDep, name: str | None = None
|
|
59
|
+
) -> Response:
|
|
60
|
+
await _known(session, sha256)
|
|
61
|
+
blob = await deps.blob_store.get_stream(fixture_blob_key(sha256))
|
|
62
|
+
media_type = (mimetypes.guess_type(name)[0] if name else None) or "application/octet-stream"
|
|
63
|
+
headers = {"Content-Length": str(blob.size), **(content_disposition(name) if name else {})}
|
|
64
|
+
return StreamingResponse(blob.chunks, media_type=media_type, headers=headers)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@router.put("/sha256/{sha256}")
|
|
68
|
+
async def upload(
|
|
69
|
+
sha256: str, request: Request, deps: RunnerDepsDep, session: SessionDep, actor: ActorDep
|
|
70
|
+
) -> dict:
|
|
71
|
+
expected = _sha(sha256)
|
|
72
|
+
stored = await blob_size(session, expected)
|
|
73
|
+
if stored is not None:
|
|
74
|
+
return {"sha256": expected, "size": stored}
|
|
75
|
+
hasher = hashlib.sha256()
|
|
76
|
+
size = 0
|
|
77
|
+
|
|
78
|
+
async def hashed() -> AsyncIterator[bytes]:
|
|
79
|
+
nonlocal size
|
|
80
|
+
async for chunk in request.stream():
|
|
81
|
+
hasher.update(chunk)
|
|
82
|
+
size += len(chunk)
|
|
83
|
+
yield chunk
|
|
84
|
+
|
|
85
|
+
staging = f"{FIXTURE_STAGING_PREFIX}{uuid.uuid4().hex}"
|
|
86
|
+
await deps.blob_store.put_stream(staging, hashed())
|
|
87
|
+
actual = hasher.hexdigest()
|
|
88
|
+
if actual != expected:
|
|
89
|
+
await deps.blob_store.delete_object(staging)
|
|
90
|
+
raise HTTPException(status_code=400, detail=f"bytes hash to {actual}, not {sha256}")
|
|
91
|
+
await deps.blob_store.copy_object(staging, fixture_blob_key(sha256))
|
|
92
|
+
await deps.blob_store.delete_object(staging)
|
|
93
|
+
await record_blob(session, sha256, size, actor.id)
|
|
94
|
+
await session.commit()
|
|
95
|
+
return {"sha256": sha256, "size": size}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
4
|
+
|
|
5
|
+
from evalkit.api.access import ActorDep, require, workspace_ref
|
|
6
|
+
from evalkit.api.deps import AgentStoreDep, RunnerDepsDep, SessionDep
|
|
7
|
+
from evalkit.api.server_keys import server_keys
|
|
8
|
+
from evalkit.core.access import can_use_agent
|
|
9
|
+
from evalkit.core.agents import explicit, server_may_drive
|
|
10
|
+
from evalkit.core.harness_check import check_harness
|
|
11
|
+
from evalkit.core.workspace import parse_slug
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/api/agents", tags=["agents"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.post("/{agent}/check")
|
|
17
|
+
async def run_check(
|
|
18
|
+
agent: str,
|
|
19
|
+
request: Request,
|
|
20
|
+
deps: RunnerDepsDep,
|
|
21
|
+
agents: AgentStoreDep,
|
|
22
|
+
session: SessionDep,
|
|
23
|
+
actor: ActorDep,
|
|
24
|
+
) -> dict:
|
|
25
|
+
definition = await agents.get(explicit(parse_slug(agent)))
|
|
26
|
+
ws = await workspace_ref(session, definition.workspace)
|
|
27
|
+
require(can_use_agent(actor, ws), f"only the owner of @{ws.name} can check {definition.ref}")
|
|
28
|
+
if definition.archived:
|
|
29
|
+
raise HTTPException(status_code=409, detail=f"agent {definition.ref} is archived")
|
|
30
|
+
if definition.harness_url is None:
|
|
31
|
+
raise HTTPException(status_code=400, detail=f"{definition.ref} has no harness_url")
|
|
32
|
+
if not server_may_drive(definition):
|
|
33
|
+
raise HTTPException(
|
|
34
|
+
status_code=400,
|
|
35
|
+
detail=f"the server does not drive {definition.ref}; its owner checks it with "
|
|
36
|
+
f"evalkit harness check {definition.ref}",
|
|
37
|
+
)
|
|
38
|
+
report = await asyncio.wait_for(
|
|
39
|
+
check_harness(
|
|
40
|
+
definition,
|
|
41
|
+
(await server_keys(request, definition, None))[0].key_for("agent", definition.ref),
|
|
42
|
+
deps.blob_store,
|
|
43
|
+
transport=deps.harness_transport,
|
|
44
|
+
sessions=request.app.state.sessions,
|
|
45
|
+
),
|
|
46
|
+
timeout=120,
|
|
47
|
+
)
|
|
48
|
+
return {"agent": agent, "checks": report}
|
evalkit/api/imports.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
|
|
8
|
+
|
|
9
|
+
from evalkit.api.access import ensure_active, load_actor, require, workspace_ref
|
|
10
|
+
from evalkit.api.auth import current_user
|
|
11
|
+
from evalkit.api.deps import PgStoreDep, RunnerDepsDep, SessionDep
|
|
12
|
+
from evalkit.api.runs import harness_client
|
|
13
|
+
from evalkit.contracts.cases import REGISTRY_FILENAME
|
|
14
|
+
from evalkit.core.access import can_write
|
|
15
|
+
from evalkit.core.agents import explicit, server_may_drive
|
|
16
|
+
from evalkit.core.fixtures import load_registries
|
|
17
|
+
from evalkit.core.importer import import_dataset, load_files
|
|
18
|
+
from evalkit.core.lint import fixture_findings
|
|
19
|
+
from evalkit.core.lint import lint as lint_cases
|
|
20
|
+
from evalkit.core.runner import RunnerDeps
|
|
21
|
+
from evalkit.core.workspace import parse_slug
|
|
22
|
+
from evalkit.db.evaluators import RemoteEvaluatorStore
|
|
23
|
+
from evalkit.db.models import User
|
|
24
|
+
from evalkit.db.workspaces import WorkspaceStore
|
|
25
|
+
|
|
26
|
+
router = APIRouter(prefix="/api", tags=["imports"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _safe_relative(name: str) -> Path:
|
|
30
|
+
parts = [p for p in Path(name).parts if p not in ("", ".")]
|
|
31
|
+
if not parts or any(p == ".." for p in parts) or Path(name).is_absolute():
|
|
32
|
+
raise HTTPException(status_code=400, detail=f"unsafe upload path: {name!r}")
|
|
33
|
+
return Path(*parts)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _save_uploads(files: list[UploadFile], directory: Path) -> list[Path]:
|
|
37
|
+
saved = []
|
|
38
|
+
for upload in files:
|
|
39
|
+
path = directory / _safe_relative(upload.filename or "cases.yaml")
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
path.write_bytes(await upload.read())
|
|
42
|
+
saved.append(path)
|
|
43
|
+
return saved
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _input_schema(raw: str) -> dict:
|
|
47
|
+
try:
|
|
48
|
+
schema = json.loads(raw)
|
|
49
|
+
except json.JSONDecodeError as exc:
|
|
50
|
+
raise HTTPException(
|
|
51
|
+
status_code=400, detail=f"input_schema is not valid JSON: {exc}"
|
|
52
|
+
) from exc
|
|
53
|
+
if not isinstance(schema, dict):
|
|
54
|
+
raise HTTPException(status_code=400, detail="input_schema must be a JSON object")
|
|
55
|
+
return schema
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _agent_input_schema(deps: RunnerDeps, agent: str) -> dict:
|
|
59
|
+
definition = await deps.agents.get(explicit(parse_slug(agent)))
|
|
60
|
+
if not server_may_drive(definition):
|
|
61
|
+
raise HTTPException(
|
|
62
|
+
status_code=400,
|
|
63
|
+
detail=f"the server does not drive {definition.ref}; evalkit lint --agent "
|
|
64
|
+
"reads its input schema from your laptop and sends it as input_schema",
|
|
65
|
+
)
|
|
66
|
+
client = await harness_client(deps, definition)
|
|
67
|
+
try:
|
|
68
|
+
caps = await client.capabilities()
|
|
69
|
+
except (httpx.HTTPError, ValueError) as exc:
|
|
70
|
+
raise HTTPException(status_code=502, detail=f"harness {agent}: {exc}") from exc
|
|
71
|
+
finally:
|
|
72
|
+
await client.aclose()
|
|
73
|
+
return caps.effective_input_schema
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.post("/lint")
|
|
77
|
+
async def lint_endpoint(
|
|
78
|
+
files: list[UploadFile],
|
|
79
|
+
deps: RunnerDepsDep,
|
|
80
|
+
store: PgStoreDep,
|
|
81
|
+
session: SessionDep,
|
|
82
|
+
request: Request,
|
|
83
|
+
dataset: str | None = None,
|
|
84
|
+
agent: str | None = None,
|
|
85
|
+
input_schema: Annotated[str | None, Form()] = None,
|
|
86
|
+
warn_unused_fixtures: bool = False,
|
|
87
|
+
) -> dict:
|
|
88
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
89
|
+
paths = await _save_uploads(files, Path(tmp))
|
|
90
|
+
sourced_cases, overlays, findings = load_files(paths)
|
|
91
|
+
registry = Path(tmp) / REGISTRY_FILENAME
|
|
92
|
+
if registry in paths:
|
|
93
|
+
entries, registry_findings = load_registries([registry])
|
|
94
|
+
findings.extend(registry_findings)
|
|
95
|
+
findings.extend(
|
|
96
|
+
fixture_findings(
|
|
97
|
+
[case for case, _ in sourced_cases],
|
|
98
|
+
entries,
|
|
99
|
+
warn_unused=warn_unused_fixtures,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
other_cases: dict[str, str] | None = None
|
|
103
|
+
if dataset is not None:
|
|
104
|
+
ref = parse_slug(dataset)
|
|
105
|
+
other_cases = await store.workspace_cases(
|
|
106
|
+
await workspace_ref(session, ref.workspace), exclude_dataset=ref.name
|
|
107
|
+
)
|
|
108
|
+
schema = await _agent_input_schema(deps, agent) if agent is not None else None
|
|
109
|
+
if input_schema is not None:
|
|
110
|
+
schema = _input_schema(input_schema)
|
|
111
|
+
known = await RemoteEvaluatorStore(request.app.state.session_factory).names()
|
|
112
|
+
findings.extend(
|
|
113
|
+
lint_cases(
|
|
114
|
+
[case for case, _ in sourced_cases],
|
|
115
|
+
[overlay for overlay, _ in overlays],
|
|
116
|
+
known_custom_evaluators=known,
|
|
117
|
+
other_cases=other_cases,
|
|
118
|
+
input_schema=schema,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return {
|
|
122
|
+
"findings": [f.__dict__ for f in findings],
|
|
123
|
+
"errors": sum(1 for f in findings if f.severity == "error"),
|
|
124
|
+
"warnings": sum(1 for f in findings if f.severity == "warning"),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.post("/sync")
|
|
129
|
+
async def sync(
|
|
130
|
+
request: Request,
|
|
131
|
+
session: SessionDep,
|
|
132
|
+
store: PgStoreDep,
|
|
133
|
+
dataset: str,
|
|
134
|
+
user: Annotated[User, Depends(current_user)],
|
|
135
|
+
files: list[UploadFile] = File(default_factory=list),
|
|
136
|
+
workspace: str | None = None,
|
|
137
|
+
prune: bool = False,
|
|
138
|
+
sha: str | None = None,
|
|
139
|
+
branch: str | None = None,
|
|
140
|
+
dirty: bool | None = None,
|
|
141
|
+
actor: str | None = None,
|
|
142
|
+
) -> dict:
|
|
143
|
+
target = workspace or user.username
|
|
144
|
+
target_ws = await workspace_ref(session, target)
|
|
145
|
+
require(can_write(await load_actor(session, user), target_ws), f"you cannot sync to @{target}")
|
|
146
|
+
ensure_active(target_ws)
|
|
147
|
+
if target_ws.is_target:
|
|
148
|
+
assert target_ws.promoted_from is not None
|
|
149
|
+
source_ws = await WorkspaceStore(request.app.state.session_factory).get_by_id(
|
|
150
|
+
target_ws.promoted_from
|
|
151
|
+
)
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=409,
|
|
154
|
+
detail=f"{target} is the promotion target of {source_ws.name}; "
|
|
155
|
+
f"sync {source_ws.name} instead",
|
|
156
|
+
)
|
|
157
|
+
if actor is not None and not user.is_admin:
|
|
158
|
+
raise HTTPException(status_code=403, detail="actor override is admin only")
|
|
159
|
+
detail = {
|
|
160
|
+
k: v
|
|
161
|
+
for k, v in {"sha": sha, "branch": branch, "dirty": dirty, "actor": actor}.items()
|
|
162
|
+
if v is not None
|
|
163
|
+
}
|
|
164
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
165
|
+
root = Path(tmp)
|
|
166
|
+
paths = await _save_uploads(files, root)
|
|
167
|
+
report = await import_dataset(
|
|
168
|
+
store,
|
|
169
|
+
workspace=target_ws,
|
|
170
|
+
dataset=dataset,
|
|
171
|
+
files=paths,
|
|
172
|
+
full=prune,
|
|
173
|
+
known_custom_evaluators=await RemoteEvaluatorStore(
|
|
174
|
+
request.app.state.session_factory
|
|
175
|
+
).names(),
|
|
176
|
+
actor_id=user.id,
|
|
177
|
+
detail=detail,
|
|
178
|
+
root=root,
|
|
179
|
+
)
|
|
180
|
+
return report.model_dump()
|
evalkit/api/limits.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from fastapi import HTTPException
|
|
6
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
7
|
+
|
|
8
|
+
from evalkit.config import MB, Config
|
|
9
|
+
|
|
10
|
+
LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class BodyLimit:
|
|
15
|
+
what: str
|
|
16
|
+
key: str
|
|
17
|
+
limit: int
|
|
18
|
+
|
|
19
|
+
def detail(self) -> str:
|
|
20
|
+
return (
|
|
21
|
+
f"{self.what} is larger than the {_size(self.limit)} limit (server config {self.key})"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _size(n: int) -> str:
|
|
26
|
+
return f"{n // MB} MB" if n % MB == 0 else f"{n} bytes"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def body_limit(cfg: Config, method: str, path: str) -> BodyLimit:
|
|
30
|
+
limits = cfg.limits
|
|
31
|
+
if method == "PUT" and re.fullmatch(r"/api/fixtures/sha256/[^/]+", path):
|
|
32
|
+
return BodyLimit(
|
|
33
|
+
"fixture upload", "limits.fixture_upload_bytes", limits.fixture_upload_bytes
|
|
34
|
+
)
|
|
35
|
+
if method == "PUT" and re.fullmatch(r"/api/experiments/\d+/blob/.+", path):
|
|
36
|
+
return BodyLimit(
|
|
37
|
+
"experiment blob", "limits.experiment_blob_bytes", limits.experiment_blob_bytes
|
|
38
|
+
)
|
|
39
|
+
if method == "POST" and path in ("/api/sync", "/api/lint"):
|
|
40
|
+
return BodyLimit("case upload", "limits.case_upload_bytes", limits.case_upload_bytes)
|
|
41
|
+
if method == "POST" and path == "/api/otel/v1/traces":
|
|
42
|
+
return BodyLimit("trace batch", "otel.max_batch_bytes", cfg.otel.max_batch_bytes)
|
|
43
|
+
return BodyLimit("request body", "limits.request_bytes", limits.request_bytes)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _log_rejection(limit: BodyLimit, method: str, path: str, received: int) -> None:
|
|
47
|
+
LOGGER.warning(
|
|
48
|
+
"request body too large: route=%s %s limit=%s key=%s received=%s",
|
|
49
|
+
method,
|
|
50
|
+
path,
|
|
51
|
+
limit.limit,
|
|
52
|
+
limit.key,
|
|
53
|
+
received,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BodyLimitMiddleware:
|
|
58
|
+
def __init__(self, app: ASGIApp, cfg: Config) -> None:
|
|
59
|
+
self.app = app
|
|
60
|
+
self.cfg = cfg
|
|
61
|
+
|
|
62
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
63
|
+
if scope["type"] != "http":
|
|
64
|
+
await self.app(scope, receive, send)
|
|
65
|
+
return
|
|
66
|
+
method, path = scope["method"], scope["path"]
|
|
67
|
+
limit = body_limit(self.cfg, method, path)
|
|
68
|
+
declared = dict(scope["headers"]).get(b"content-length", b"")
|
|
69
|
+
received = 0
|
|
70
|
+
|
|
71
|
+
async def capped() -> Message:
|
|
72
|
+
nonlocal received
|
|
73
|
+
if declared.isdigit() and int(declared) > limit.limit:
|
|
74
|
+
_log_rejection(limit, method, path, int(declared))
|
|
75
|
+
raise HTTPException(status_code=413, detail=limit.detail())
|
|
76
|
+
message = await receive()
|
|
77
|
+
if message["type"] == "http.request":
|
|
78
|
+
received += len(message.get("body", b""))
|
|
79
|
+
if received > limit.limit:
|
|
80
|
+
_log_rejection(limit, method, path, received)
|
|
81
|
+
raise HTTPException(status_code=413, detail=limit.detail())
|
|
82
|
+
return message
|
|
83
|
+
|
|
84
|
+
await self.app(scope, capped, send)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from evalkit.api.auth import AdminUser
|
|
5
|
+
from evalkit.api.deps import ConfigDep, KeysDep
|
|
6
|
+
from evalkit.contracts.settings import JudgeSettings
|
|
7
|
+
from evalkit.core.endpoints import check_endpoint
|
|
8
|
+
from evalkit.db.llm_providers import LlmProviderStore
|
|
9
|
+
|
|
10
|
+
router = APIRouter(tags=["llm providers"])
|
|
11
|
+
|
|
12
|
+
NO_DEFAULT_JUDGE = "no default judge; an admin sets one with evalkit judge-default set"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _store(request: Request) -> LlmProviderStore:
|
|
16
|
+
return request.app.state.llm_providers
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def require_registered_judge(providers: LlmProviderStore, base_url: str) -> None:
|
|
20
|
+
if await providers.active_by_base_url(base_url) is None:
|
|
21
|
+
registered = ", ".join(p.base_url for p in await providers.list_providers())
|
|
22
|
+
raise HTTPException(
|
|
23
|
+
status_code=400,
|
|
24
|
+
detail=f"{base_url} is not a registered LLM provider; "
|
|
25
|
+
f"registered: {registered or 'none'}",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("/api/llm-providers")
|
|
30
|
+
async def list_llm_providers(request: Request, include_archived: bool = False) -> list[dict]:
|
|
31
|
+
rows = await _store(request).list_providers(include_archived=include_archived)
|
|
32
|
+
return [row.model_dump(mode="json") for row in rows]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/api/llm-providers/{name}")
|
|
36
|
+
async def get_llm_provider(name: str, request: Request) -> dict:
|
|
37
|
+
return (await _store(request).get(name)).model_dump(mode="json")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CreateLlmProvider(BaseModel):
|
|
41
|
+
name: str
|
|
42
|
+
base_url: str
|
|
43
|
+
api_key: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.post("/api/llm-providers")
|
|
47
|
+
async def create_llm_provider(
|
|
48
|
+
body: CreateLlmProvider, request: Request, keys: KeysDep, cfg: ConfigDep, admin: AdminUser
|
|
49
|
+
) -> dict:
|
|
50
|
+
await check_endpoint(body.base_url, allow_private=cfg.allow_private_endpoints)
|
|
51
|
+
created = await _store(request).create(body.name, body.base_url, keys.seal(body.api_key))
|
|
52
|
+
return created.model_dump(mode="json")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class EditLlmProvider(BaseModel):
|
|
56
|
+
base_url: str | None = None
|
|
57
|
+
api_key: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.patch("/api/llm-providers/{name}")
|
|
61
|
+
async def edit_llm_provider(
|
|
62
|
+
name: str,
|
|
63
|
+
body: EditLlmProvider,
|
|
64
|
+
request: Request,
|
|
65
|
+
keys: KeysDep,
|
|
66
|
+
cfg: ConfigDep,
|
|
67
|
+
admin: AdminUser,
|
|
68
|
+
) -> dict:
|
|
69
|
+
current = await _store(request).get(name)
|
|
70
|
+
if body.base_url is not None and body.base_url != current.base_url:
|
|
71
|
+
await check_endpoint(body.base_url, allow_private=cfg.allow_private_endpoints)
|
|
72
|
+
edited = await _store(request).edit(
|
|
73
|
+
name,
|
|
74
|
+
base_url=body.base_url,
|
|
75
|
+
api_key_encrypted=keys.seal(body.api_key),
|
|
76
|
+
clear_api_key="api_key" in body.model_fields_set and not body.api_key,
|
|
77
|
+
)
|
|
78
|
+
return edited.model_dump(mode="json")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post("/api/llm-providers/{name}/archive")
|
|
82
|
+
async def archive_llm_provider(name: str, request: Request, admin: AdminUser) -> dict:
|
|
83
|
+
return (await _store(request).archive(name)).model_dump(mode="json")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.post("/api/llm-providers/{name}/restore")
|
|
87
|
+
async def restore_llm_provider(name: str, request: Request, admin: AdminUser) -> dict:
|
|
88
|
+
return (await _store(request).restore(name)).model_dump(mode="json")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.get("/api/judge-default")
|
|
92
|
+
async def get_judge_default(request: Request) -> dict:
|
|
93
|
+
judge = await _store(request).default_judge()
|
|
94
|
+
if judge is None:
|
|
95
|
+
raise HTTPException(status_code=404, detail=NO_DEFAULT_JUDGE)
|
|
96
|
+
return judge.model_dump()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.put("/api/judge-default")
|
|
100
|
+
async def set_judge_default(body: JudgeSettings, request: Request, admin: AdminUser) -> dict:
|
|
101
|
+
return (await _store(request).set_default_judge(body)).model_dump()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class JudgePatch(BaseModel):
|
|
105
|
+
base_url: str | None = None
|
|
106
|
+
model: str | None = None
|
|
107
|
+
reasoning_effort: str | None = None
|
|
108
|
+
max_tokens: int | None = None
|
|
109
|
+
concurrency: int | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.patch("/api/judge-default")
|
|
113
|
+
async def patch_judge_default(body: JudgePatch, request: Request, admin: AdminUser) -> dict:
|
|
114
|
+
current = await _store(request).default_judge()
|
|
115
|
+
merged = {
|
|
116
|
+
**(current.model_dump() if current is not None else {}),
|
|
117
|
+
**body.model_dump(exclude_none=True),
|
|
118
|
+
}
|
|
119
|
+
judge = JudgeSettings.model_validate(merged)
|
|
120
|
+
return (await _store(request).set_default_judge(judge)).model_dump()
|
|
File without changes
|
evalkit/api/mcp/auth.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
from fastmcp.server.auth import (
|
|
3
|
+
AccessToken,
|
|
4
|
+
AuthProvider,
|
|
5
|
+
MultiAuth,
|
|
6
|
+
RemoteAuthProvider,
|
|
7
|
+
TokenVerifier,
|
|
8
|
+
)
|
|
9
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
11
|
+
|
|
12
|
+
from evalkit.api.auth import bearer_login, token_expiry
|
|
13
|
+
from evalkit.config import Config
|
|
14
|
+
|
|
15
|
+
SCOPE_READ = "evalkit:read"
|
|
16
|
+
MCP_PATH = "/mcp"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resource_url(cfg: Config) -> str:
|
|
20
|
+
return f"{cfg.server.base_url.rstrip('/')}{MCP_PATH}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# An evalkit login token reaches MCP with the read scope; agent tokens stay
|
|
24
|
+
# collector-only here as on /api.
|
|
25
|
+
class EvalkitTokenVerifier(TokenVerifier):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
session_factory: async_sessionmaker[AsyncSession],
|
|
29
|
+
base_url: str,
|
|
30
|
+
required_scopes: list[str] | None = None,
|
|
31
|
+
):
|
|
32
|
+
super().__init__(base_url=base_url, required_scopes=required_scopes)
|
|
33
|
+
self._session_factory = session_factory
|
|
34
|
+
|
|
35
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
36
|
+
async with self._session_factory() as session:
|
|
37
|
+
try:
|
|
38
|
+
row, user = await bearer_login(session, token)
|
|
39
|
+
except HTTPException:
|
|
40
|
+
return None
|
|
41
|
+
await session.commit()
|
|
42
|
+
expires = token_expiry(row)
|
|
43
|
+
return AccessToken(
|
|
44
|
+
token=token,
|
|
45
|
+
client_id=f"evalkit:{user.username}",
|
|
46
|
+
scopes=[SCOPE_READ],
|
|
47
|
+
expires_at=int(expires.timestamp()) if expires is not None else None,
|
|
48
|
+
claims={"sub": user.username, "evalkit_user": user.username},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# The identity provider names the scope (`mcp.oauth.scope`); a token carrying it
|
|
53
|
+
# gets evalkit's own read scope, which the tools check.
|
|
54
|
+
class ProviderScopeVerifier(TokenVerifier):
|
|
55
|
+
def __init__(self, inner: TokenVerifier, scope: str, base_url: str):
|
|
56
|
+
super().__init__(base_url=base_url)
|
|
57
|
+
self._inner = inner
|
|
58
|
+
self._scope = scope
|
|
59
|
+
|
|
60
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
61
|
+
found = await self._inner.verify_token(token)
|
|
62
|
+
if found is None:
|
|
63
|
+
return None
|
|
64
|
+
scopes = [s for s in found.scopes if s != SCOPE_READ]
|
|
65
|
+
if self._scope in found.scopes:
|
|
66
|
+
scopes.append(SCOPE_READ)
|
|
67
|
+
return found.model_copy(update={"scopes": scopes})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_auth(
|
|
71
|
+
cfg: Config,
|
|
72
|
+
session_factory: async_sessionmaker[AsyncSession],
|
|
73
|
+
jwt_verifier: TokenVerifier | None = None,
|
|
74
|
+
) -> AuthProvider:
|
|
75
|
+
oauth = cfg.mcp.oauth
|
|
76
|
+
if oauth is None:
|
|
77
|
+
return EvalkitTokenVerifier(
|
|
78
|
+
session_factory, cfg.server.base_url, required_scopes=[SCOPE_READ]
|
|
79
|
+
)
|
|
80
|
+
verifier = jwt_verifier or JWTVerifier(
|
|
81
|
+
jwks_uri=oauth.jwks_uri,
|
|
82
|
+
issuer=oauth.issuer,
|
|
83
|
+
audience=oauth.audience or resource_url(cfg),
|
|
84
|
+
algorithm=oauth.algorithm,
|
|
85
|
+
)
|
|
86
|
+
remote = RemoteAuthProvider(
|
|
87
|
+
token_verifier=ProviderScopeVerifier(verifier, oauth.scope, cfg.server.base_url),
|
|
88
|
+
# As configured: AnyHttpUrl would turn https://host into https://host/, which clients
|
|
89
|
+
# cannot match to the provider's issuer (RFC 8414).
|
|
90
|
+
authorization_servers=[oauth.issuer], # type: ignore[list-item]
|
|
91
|
+
base_url=cfg.server.base_url,
|
|
92
|
+
scopes_supported=[oauth.scope],
|
|
93
|
+
resource_name="evalkit",
|
|
94
|
+
)
|
|
95
|
+
return MultiAuth(
|
|
96
|
+
server=remote,
|
|
97
|
+
verifiers={"evalkit": EvalkitTokenVerifier(session_factory, cfg.server.base_url)},
|
|
98
|
+
server_source_id="oauth",
|
|
99
|
+
required_scopes=[SCOPE_READ],
|
|
100
|
+
)
|