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/cli/main.py
ADDED
|
@@ -0,0 +1,1365 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import time
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from importlib.util import find_spec
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import typer
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
|
|
15
|
+
from evalkit import version
|
|
16
|
+
from evalkit.cli import init_server
|
|
17
|
+
from evalkit.cli.admin_commands import register as register_admin
|
|
18
|
+
from evalkit.cli.client import ApiClient, ApiError
|
|
19
|
+
from evalkit.cli.local_run import (
|
|
20
|
+
check_local,
|
|
21
|
+
evaluate_local,
|
|
22
|
+
local_input_schema,
|
|
23
|
+
mark_interrupted,
|
|
24
|
+
run_local,
|
|
25
|
+
)
|
|
26
|
+
from evalkit.cli.local_run import (
|
|
27
|
+
credentials as local_credentials,
|
|
28
|
+
)
|
|
29
|
+
from evalkit.cli.runs_commands import register as register_runs
|
|
30
|
+
from evalkit.cli.skill_targets import INSTALL_MANIFEST
|
|
31
|
+
from evalkit.cli.skill_targets import record as record_skill_target
|
|
32
|
+
from evalkit.cli.tokens_commands import register as register_tokens
|
|
33
|
+
from evalkit.cli.traces_commands import register as register_traces
|
|
34
|
+
from evalkit.cli.update import run_update
|
|
35
|
+
from evalkit.client import connection
|
|
36
|
+
from evalkit.client.agents import definition_of
|
|
37
|
+
from evalkit.client.api import ApiRequestError, VersionMismatch, encode
|
|
38
|
+
from evalkit.client.summary import summary_lines
|
|
39
|
+
from evalkit.client.sync import git_revision, resolve_path, sync_dataset
|
|
40
|
+
from evalkit.config import Config, LocalConfig, MissingEnvError, load_config
|
|
41
|
+
from evalkit.contracts.cases import REGISTRY_FILENAME, is_case_yaml, load_cases_from_yaml
|
|
42
|
+
from evalkit.contracts.settings import JudgeSettings
|
|
43
|
+
from evalkit.core.agents import AgentDefinition, Driver, driver, explicit, server_may_drive
|
|
44
|
+
from evalkit.core.fixtures import load_registries
|
|
45
|
+
from evalkit.core.harness import HarnessClient
|
|
46
|
+
from evalkit.core.lint import fixture_findings
|
|
47
|
+
from evalkit.core.lint import lint as lint_cases
|
|
48
|
+
from evalkit.core.review import CaseState
|
|
49
|
+
from evalkit.core.runner import ScoringFailed, dropped_cases_text
|
|
50
|
+
from evalkit.core.workspace import Reference, parse_reference, slug
|
|
51
|
+
from evalkit.credentials import MissingCredential
|
|
52
|
+
from evalkit.paths import contracts_root, docs_root, skills_root
|
|
53
|
+
from evalkit.store.base import GitRevision, Selection
|
|
54
|
+
|
|
55
|
+
# `serve` and `db upgrade` need the `evalkit-py[server]` extra; the base install
|
|
56
|
+
# is a thin HTTP client. The boundary is explicit here rather than hidden in
|
|
57
|
+
# per-command imports.
|
|
58
|
+
if find_spec("uvicorn") and find_spec("sqlalchemy"):
|
|
59
|
+
from evalkit.cli import server_commands
|
|
60
|
+
else:
|
|
61
|
+
server_commands = None # type: ignore[assignment]
|
|
62
|
+
|
|
63
|
+
RUN_POLL_SECONDS = 2.0
|
|
64
|
+
|
|
65
|
+
app = typer.Typer(
|
|
66
|
+
name="evalkit",
|
|
67
|
+
no_args_is_help=True,
|
|
68
|
+
epilog="Bugs, feature requests and feedback: https://github.com/inferedge-ai/evalkit/issues",
|
|
69
|
+
)
|
|
70
|
+
datasets_app = typer.Typer(no_args_is_help=True, help="List datasets and their cases.")
|
|
71
|
+
harness_app = typer.Typer(no_args_is_help=True, help="Verify a client's Harness API.")
|
|
72
|
+
users_app = typer.Typer(no_args_is_help=True, help="Manage server users (admin only).")
|
|
73
|
+
skills_app = typer.Typer(no_args_is_help=True, help="Install the bundled agent skills.")
|
|
74
|
+
contracts_app = typer.Typer(no_args_is_help=True, help="Export the bundled API contracts.")
|
|
75
|
+
db_app = typer.Typer(no_args_is_help=True, help="Server database schema upgrades (server only).")
|
|
76
|
+
review_app = typer.Typer(no_args_is_help=True, help="Request, give and read case reviews.")
|
|
77
|
+
app.add_typer(datasets_app, name="datasets")
|
|
78
|
+
app.add_typer(harness_app, name="harness")
|
|
79
|
+
app.add_typer(users_app, name="users")
|
|
80
|
+
app.add_typer(skills_app, name="skills")
|
|
81
|
+
app.add_typer(contracts_app, name="contracts")
|
|
82
|
+
app.add_typer(db_app, name="db")
|
|
83
|
+
app.add_typer(review_app, name="review")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _print_version(value: bool) -> None:
|
|
87
|
+
if value:
|
|
88
|
+
typer.echo(version.current())
|
|
89
|
+
raise typer.Exit()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.callback()
|
|
93
|
+
def main(
|
|
94
|
+
show_version: bool = typer.Option(
|
|
95
|
+
False, "--version", callback=_print_version, is_eager=True, help="Print the CLI version."
|
|
96
|
+
),
|
|
97
|
+
) -> None:
|
|
98
|
+
"""evalkit — self-hosted agent evals. Runs execute here against your local agent;
|
|
99
|
+
results land on the server."""
|
|
100
|
+
logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _active_connection() -> connection.Connection:
|
|
104
|
+
try:
|
|
105
|
+
return connection.active()
|
|
106
|
+
except LookupError as exc:
|
|
107
|
+
raise ApiError(str(exc)) from exc
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _client() -> ApiClient:
|
|
111
|
+
active = _active_connection()
|
|
112
|
+
return ApiClient(active.url, active.token)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _connection() -> connection.Connection:
|
|
116
|
+
active = _active_connection()
|
|
117
|
+
me = _client().get("/api/auth/me").json()
|
|
118
|
+
warning = connection.token_expiry_warning(active, me.get("token_expires_at"), datetime.now(UTC))
|
|
119
|
+
if warning is not None:
|
|
120
|
+
typer.secho(warning, fg="yellow", err=True)
|
|
121
|
+
return active._replace(username=me["username"])
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _agent_slug(text: str) -> str:
|
|
125
|
+
return slug(_parse(text, _connection().username or ""))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
register_tokens(app, _client)
|
|
129
|
+
register_admin(app, _client, lambda: _connection().username or "")
|
|
130
|
+
register_traces(app, _client, _agent_slug)
|
|
131
|
+
register_runs(
|
|
132
|
+
app,
|
|
133
|
+
lambda: _client(),
|
|
134
|
+
lambda text: _agent_definition(_parse(text, _connection().username or "")),
|
|
135
|
+
lambda definition: HarnessClient(
|
|
136
|
+
definition,
|
|
137
|
+
local_credentials(_with_secrets(_local_config(), scoring=False), _connection()).key_for(
|
|
138
|
+
"agent", definition.ref
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _parse(text: str, workspace: str) -> Reference:
|
|
145
|
+
try:
|
|
146
|
+
return parse_reference(text, workspace)
|
|
147
|
+
except ValueError as exc:
|
|
148
|
+
raise ApiError(str(exc)) from exc
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _resolve(arg: str, cases_root: Path | None, workspace: str):
|
|
152
|
+
try:
|
|
153
|
+
return resolve_path(arg, cases_root, workspace)
|
|
154
|
+
except ValueError as exc:
|
|
155
|
+
raise ApiError(str(exc)) from exc
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _find_local() -> LocalConfig | None:
|
|
159
|
+
try:
|
|
160
|
+
return LocalConfig.find(Path.cwd())
|
|
161
|
+
except ValueError as exc:
|
|
162
|
+
raise ApiError(str(exc)) from exc
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _local_config() -> LocalConfig:
|
|
166
|
+
local = _find_local()
|
|
167
|
+
if local is None:
|
|
168
|
+
raise ApiError("no evalkit.yaml found above the current directory")
|
|
169
|
+
return local
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _with_secrets(local: LocalConfig, *, scoring: bool) -> LocalConfig:
|
|
173
|
+
try:
|
|
174
|
+
return local.resolve_env(scoring=scoring)
|
|
175
|
+
except MissingEnvError as exc:
|
|
176
|
+
raise ApiError(str(exc)) from exc
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _server_config(path: Path) -> Config:
|
|
180
|
+
load_dotenv(path.parent / ".env", override=False)
|
|
181
|
+
try:
|
|
182
|
+
return load_config(path)
|
|
183
|
+
except ValueError as exc:
|
|
184
|
+
raise ApiError(str(exc)) from exc
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _relative_files(
|
|
188
|
+
path: Path, patterns: tuple[str, ...], *, allow_empty: bool = False
|
|
189
|
+
) -> list[tuple[str, Path]]:
|
|
190
|
+
if path.is_dir():
|
|
191
|
+
matched = sorted(
|
|
192
|
+
f
|
|
193
|
+
for pattern in patterns
|
|
194
|
+
for f in path.rglob(pattern)
|
|
195
|
+
if f.is_file() and not any(part.startswith(".") for part in f.relative_to(path).parts)
|
|
196
|
+
)
|
|
197
|
+
files = [(f.relative_to(path).as_posix(), f) for f in matched]
|
|
198
|
+
else:
|
|
199
|
+
files = [(path.name, path)]
|
|
200
|
+
if not allow_empty and not any(name.endswith((".yaml", ".yml")) for name, _ in files):
|
|
201
|
+
raise ApiError(f"no YAML files under {path}")
|
|
202
|
+
return files
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _yaml_files(path: Path, *, allow_empty: bool = False) -> list[tuple[str, Path]]:
|
|
206
|
+
files = _relative_files(path, ("*.yaml", "*.yml"), allow_empty=allow_empty)
|
|
207
|
+
if path.is_dir():
|
|
208
|
+
files = [(name, p) for name, p in files if is_case_yaml(p.relative_to(path))]
|
|
209
|
+
return files
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _nearest_registry(path: Path) -> Path | None:
|
|
213
|
+
start = path if path.is_dir() else path.parent
|
|
214
|
+
for directory in (start, *start.parents):
|
|
215
|
+
candidate = directory / REGISTRY_FILENAME
|
|
216
|
+
if candidate.is_file():
|
|
217
|
+
return candidate
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _print_findings(findings: list[dict]) -> None:
|
|
222
|
+
for f in findings:
|
|
223
|
+
marker = "✗" if f["severity"] == "error" else "⚠"
|
|
224
|
+
typer.echo(f" {marker} {f['code']} [{f['case_id']}] {f['message']}")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@app.command()
|
|
228
|
+
def login(
|
|
229
|
+
url: str = typer.Option(None, "--url", help="Server base URL, e.g. https://evalkit.internal"),
|
|
230
|
+
username: str = typer.Option(None, "--username"),
|
|
231
|
+
password: str = typer.Option(None, "--password"),
|
|
232
|
+
):
|
|
233
|
+
"""Log in to a server and store the login in ~/.evalkit/config.toml, replacing any
|
|
234
|
+
earlier one.
|
|
235
|
+
|
|
236
|
+
Example: evalkit login --url https://evalkit.example.com
|
|
237
|
+
"""
|
|
238
|
+
url = url or typer.prompt("Server URL")
|
|
239
|
+
username = username or typer.prompt("Username")
|
|
240
|
+
password = password or typer.prompt("Password", hide_input=True)
|
|
241
|
+
body = (
|
|
242
|
+
ApiClient(url)
|
|
243
|
+
.post("/api/auth/login", json={"username": username, "password": password})
|
|
244
|
+
.json()
|
|
245
|
+
)
|
|
246
|
+
path = connection.save(connection.Connection(url, body["token"], body["username"]))
|
|
247
|
+
typer.echo(f"logged in as {username} on {url}; saved to {path}")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@app.command()
|
|
251
|
+
def whoami():
|
|
252
|
+
"""Check the stored token: who it logs in as and when it expires.
|
|
253
|
+
|
|
254
|
+
Exits 1 when the server rejects the token. Run it before a long run or
|
|
255
|
+
before handing the token to an exporter. Example: evalkit whoami
|
|
256
|
+
"""
|
|
257
|
+
active = _active_connection()
|
|
258
|
+
me = _client().get("/api/auth/me").json()
|
|
259
|
+
source = " (from EVALKIT_SERVER_URL)" if active.from_env else ""
|
|
260
|
+
typer.echo(f"{me['username']} on {active.url}{source}")
|
|
261
|
+
expires_at = me.get("token_expires_at")
|
|
262
|
+
if expires_at is None:
|
|
263
|
+
typer.echo("token does not expire")
|
|
264
|
+
else:
|
|
265
|
+
expires = datetime.fromisoformat(expires_at).astimezone(UTC)
|
|
266
|
+
typer.echo(f"token expires {expires:%Y-%m-%d %H:%M} UTC")
|
|
267
|
+
warning = connection.token_expiry_warning(active, expires_at, datetime.now(UTC))
|
|
268
|
+
if warning is not None:
|
|
269
|
+
typer.secho(warning, fg="yellow", err=True)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.command()
|
|
273
|
+
def update(
|
|
274
|
+
server: str = typer.Option(
|
|
275
|
+
None, "--server", help="Server base URL; defaults to the one you logged in to."
|
|
276
|
+
),
|
|
277
|
+
skills: bool = typer.Option(
|
|
278
|
+
True,
|
|
279
|
+
"--skills/--no-skills",
|
|
280
|
+
help="After reinstalling, refresh the skills in every directory "
|
|
281
|
+
"`evalkit skills install` wrote to.",
|
|
282
|
+
),
|
|
283
|
+
):
|
|
284
|
+
"""Reinstall the CLI from PyPI at the server's version.
|
|
285
|
+
|
|
286
|
+
Run it from the project directory: it also moves an `evalkit-py==…` pin in
|
|
287
|
+
./pyproject.toml to the server's version and runs `uv sync`, so the CLI and the SDK
|
|
288
|
+
match the server again after a deploy. After a reinstall it runs the new
|
|
289
|
+
`evalkit skills install` again in every skills directory installed before, and in
|
|
290
|
+
./.claude/skills or ./.agents/skills if an earlier install put skills there.
|
|
291
|
+
|
|
292
|
+
Example: evalkit update · evalkit update --no-skills
|
|
293
|
+
"""
|
|
294
|
+
server_url = server or _active_connection().url
|
|
295
|
+
run_update(ApiClient(server_url, check_version=False), skills=skills)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@app.command()
|
|
299
|
+
def lint(
|
|
300
|
+
path: Path = typer.Argument(..., help="Case YAML file or directory."),
|
|
301
|
+
offline: bool = typer.Option(
|
|
302
|
+
False,
|
|
303
|
+
"--offline",
|
|
304
|
+
help="Lint locally without a server: the no-server subset of the server lint. "
|
|
305
|
+
"Skips the checks that need server state (registered custom evaluators, "
|
|
306
|
+
"overlay targets).",
|
|
307
|
+
),
|
|
308
|
+
dataset: str | None = typer.Option(
|
|
309
|
+
None,
|
|
310
|
+
"--dataset",
|
|
311
|
+
help="Dataset these cases sync into. With it, the server also checks overlay "
|
|
312
|
+
"targets against the other datasets in its workspace.",
|
|
313
|
+
),
|
|
314
|
+
agent: str | None = typer.Option(
|
|
315
|
+
None,
|
|
316
|
+
"--agent",
|
|
317
|
+
help="Agent to check case inputs against (its input_schema). Needs the server; "
|
|
318
|
+
"on demand, not for CI.",
|
|
319
|
+
),
|
|
320
|
+
):
|
|
321
|
+
"""Lint case YAML without importing it (server-side by default).
|
|
322
|
+
|
|
323
|
+
The server lint also checks fixture keys against the nearest fixtures.yaml,
|
|
324
|
+
so it covers everything --offline does.
|
|
325
|
+
|
|
326
|
+
Example: evalkit lint ./eval-cases --dataset client-cases · evalkit lint --offline datasets
|
|
327
|
+
· evalkit lint ./eval-cases --agent support-triage
|
|
328
|
+
"""
|
|
329
|
+
if offline and agent is not None:
|
|
330
|
+
raise ApiError("--agent needs the server; drop --offline")
|
|
331
|
+
if offline:
|
|
332
|
+
all_cases, all_overlays = [], []
|
|
333
|
+
for _, file_path in _yaml_files(path):
|
|
334
|
+
cases, overlays = load_cases_from_yaml(file_path)
|
|
335
|
+
all_cases.extend(cases)
|
|
336
|
+
all_overlays.extend(overlays)
|
|
337
|
+
known = {name for case in all_cases for name in case.evaluators.custom}
|
|
338
|
+
known.update(name for ov in all_overlays for name in ov.evaluators.custom)
|
|
339
|
+
findings = lint_cases(all_cases, all_overlays, known_custom_evaluators=known)
|
|
340
|
+
registry = _nearest_registry(path)
|
|
341
|
+
entries, registry_findings = load_registries([registry] if registry else [])
|
|
342
|
+
findings.extend(registry_findings)
|
|
343
|
+
registry_in_scope = registry is not None and registry.parent == path
|
|
344
|
+
findings.extend(fixture_findings(all_cases, entries, warn_unused=registry_in_scope))
|
|
345
|
+
_print_findings([f.__dict__ for f in findings])
|
|
346
|
+
errors = sum(1 for f in findings if f.severity == "error")
|
|
347
|
+
warnings = sum(1 for f in findings if f.severity == "warning")
|
|
348
|
+
typer.echo(f"{len(all_cases)} case(s): {errors} error(s), {warnings} warning(s)")
|
|
349
|
+
if errors:
|
|
350
|
+
raise typer.Exit(code=1)
|
|
351
|
+
return
|
|
352
|
+
conn = _connection()
|
|
353
|
+
me = conn.username or ""
|
|
354
|
+
params = {"dataset": slug(_parse(dataset, me))} if dataset else {}
|
|
355
|
+
form: dict[str, str] = {}
|
|
356
|
+
if agent:
|
|
357
|
+
ref = _parse(agent, me)
|
|
358
|
+
definition = _agent_definition(ref)
|
|
359
|
+
_require_own_agent(definition, me, "lint against")
|
|
360
|
+
if server_may_drive(definition):
|
|
361
|
+
params["agent"] = slug(ref)
|
|
362
|
+
else:
|
|
363
|
+
local = _with_secrets(_local_config(), scoring=False)
|
|
364
|
+
try:
|
|
365
|
+
schema = asyncio.run(local_input_schema(conn, local, ref))
|
|
366
|
+
except (httpx.HTTPError, ValueError, ApiRequestError) as exc:
|
|
367
|
+
raise ApiError(f"harness {definition.ref}: {exc}") from exc
|
|
368
|
+
form["input_schema"] = json.dumps(schema)
|
|
369
|
+
files = _yaml_files(path)
|
|
370
|
+
registry = _nearest_registry(path)
|
|
371
|
+
if registry is not None:
|
|
372
|
+
files.append((REGISTRY_FILENAME, registry))
|
|
373
|
+
params["warn_unused_fixtures"] = str(registry.parent == path).lower()
|
|
374
|
+
body = _client().post_files("/api/lint", files, params=params, data=form).json()
|
|
375
|
+
_print_findings(body["findings"])
|
|
376
|
+
typer.echo(f"{body['errors']} error(s), {body['warnings']} warning(s)")
|
|
377
|
+
if body["errors"]:
|
|
378
|
+
raise typer.Exit(code=1)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _sync_and_report(
|
|
382
|
+
dataset_dir: Path,
|
|
383
|
+
*,
|
|
384
|
+
name: str,
|
|
385
|
+
workspace: str,
|
|
386
|
+
revision: GitRevision,
|
|
387
|
+
subpath: str | None = None,
|
|
388
|
+
fixtures_dir: Path | None = None,
|
|
389
|
+
actor: str | None = None,
|
|
390
|
+
) -> None:
|
|
391
|
+
try:
|
|
392
|
+
report = sync_dataset(
|
|
393
|
+
_client(),
|
|
394
|
+
dataset_dir,
|
|
395
|
+
name=name,
|
|
396
|
+
workspace=workspace,
|
|
397
|
+
revision=revision,
|
|
398
|
+
subpath=subpath,
|
|
399
|
+
fixtures_dir=fixtures_dir,
|
|
400
|
+
actor=actor,
|
|
401
|
+
)
|
|
402
|
+
except ValueError as exc:
|
|
403
|
+
raise ApiError(str(exc)) from exc
|
|
404
|
+
_print_findings(report["findings"])
|
|
405
|
+
if report["aborted"]:
|
|
406
|
+
typer.secho("sync aborted on lint errors", fg="red")
|
|
407
|
+
raise typer.Exit(code=1)
|
|
408
|
+
typer.echo(
|
|
409
|
+
f"synced {slug(Reference(name, workspace))}: created={report['created']} "
|
|
410
|
+
f"patched={report['patched']} deleted={report['deleted']} "
|
|
411
|
+
f"overlays_applied={report['overlays_applied']}"
|
|
412
|
+
)
|
|
413
|
+
review = report.get("review") or {}
|
|
414
|
+
if review.get("now_unreviewed"):
|
|
415
|
+
typer.echo(
|
|
416
|
+
f"{len(review['now_unreviewed'])} approved case(s) changed and are now unreviewed: "
|
|
417
|
+
+ ", ".join(review["now_unreviewed"])
|
|
418
|
+
)
|
|
419
|
+
if review.get("carried"):
|
|
420
|
+
typer.echo(
|
|
421
|
+
f"{len(review['carried'])} case(s) updated and still under review: "
|
|
422
|
+
+ ", ".join(f"{c['case_id']} ({c['reviewer']})" for c in review["carried"])
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@app.command()
|
|
427
|
+
def sync(
|
|
428
|
+
path: str = typer.Argument(
|
|
429
|
+
..., help="Dataset name under the cases root, or a directory or case file on disk."
|
|
430
|
+
),
|
|
431
|
+
workspace: str = typer.Option(
|
|
432
|
+
None,
|
|
433
|
+
"--workspace",
|
|
434
|
+
help="Target workspace; defaults to yours. CI passes the shared one, e.g. dev.",
|
|
435
|
+
),
|
|
436
|
+
version: str | None = typer.Option(
|
|
437
|
+
None, "--version", help="Git sha label; detected from git when omitted."
|
|
438
|
+
),
|
|
439
|
+
branch: str | None = typer.Option(None, "--branch", help="Detected from git when omitted."),
|
|
440
|
+
dirty: bool | None = typer.Option(
|
|
441
|
+
None, "--dirty/--no-dirty", help="Detected from git when omitted."
|
|
442
|
+
),
|
|
443
|
+
actor: str | None = typer.Option(
|
|
444
|
+
None, "--actor", help="Record this user as the author of the sync (admin only)."
|
|
445
|
+
),
|
|
446
|
+
):
|
|
447
|
+
"""Import a dataset into a workspace. The dataset is the directory under the cases
|
|
448
|
+
root (or the directory itself without an evalkit.yaml); a whole dataset prunes cases no
|
|
449
|
+
longer on disk, a subfolder or file patches just its cases. Uploads case YAML plus the
|
|
450
|
+
nearest fixtures.yaml.
|
|
451
|
+
|
|
452
|
+
Example: evalkit sync finance · evalkit sync finance/statement-analysis ·
|
|
453
|
+
evalkit sync datasets/finance --workspace dev --version $GITHUB_SHA
|
|
454
|
+
"""
|
|
455
|
+
conn = _connection()
|
|
456
|
+
me = conn.username or ""
|
|
457
|
+
target = workspace or me
|
|
458
|
+
local = _find_local()
|
|
459
|
+
resolved = _resolve(path, local.cases if local else None, me)
|
|
460
|
+
if resolved.dataset_dir is None:
|
|
461
|
+
raise ApiError(f"{path} is not on disk; sync needs a directory or file")
|
|
462
|
+
detected = git_revision(resolved.dataset_dir)
|
|
463
|
+
revision = GitRevision(
|
|
464
|
+
sha=version if version is not None else detected.sha,
|
|
465
|
+
branch=branch if branch is not None else detected.branch,
|
|
466
|
+
dirty=dirty if dirty is not None else detected.dirty,
|
|
467
|
+
)
|
|
468
|
+
_sync_and_report(
|
|
469
|
+
resolved.dataset_dir,
|
|
470
|
+
name=resolved.ref.name,
|
|
471
|
+
workspace=target,
|
|
472
|
+
revision=revision,
|
|
473
|
+
subpath=resolved.subpath,
|
|
474
|
+
fixtures_dir=local.fixtures if local else None,
|
|
475
|
+
actor=actor,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@datasets_app.command("create")
|
|
480
|
+
def datasets_create(
|
|
481
|
+
dataset: str = typer.Argument(..., help="finance (yours) or finance@prod."),
|
|
482
|
+
):
|
|
483
|
+
"""Create an empty suite dataset (writers of the workspace).
|
|
484
|
+
|
|
485
|
+
A promotion target is fed by approvals in its source workspace, so its datasets
|
|
486
|
+
start here. Example: evalkit datasets create finance@prod
|
|
487
|
+
"""
|
|
488
|
+
ref = _parse(dataset, _connection().username or "")
|
|
489
|
+
_client().post("/api/datasets", json={"name": ref.name, "workspace": ref.workspace})
|
|
490
|
+
typer.echo(f"created {slug(ref)}")
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@datasets_app.command("list")
|
|
494
|
+
def datasets_list(
|
|
495
|
+
dataset: str | None = typer.Argument(
|
|
496
|
+
None, help="List this dataset's cases instead: finance (yours) or finance@dev."
|
|
497
|
+
),
|
|
498
|
+
state: CaseState | None = typer.Option(
|
|
499
|
+
None, "--state", help="Only cases in this review state (shared workspaces only)."
|
|
500
|
+
),
|
|
501
|
+
tag: str | None = typer.Option(None, "--tag", help="Only cases carrying this tag."),
|
|
502
|
+
):
|
|
503
|
+
"""List datasets with kind and version, or one dataset's cases.
|
|
504
|
+
|
|
505
|
+
With a dataset, prints each case's id, review state (shared workspaces only; `-`
|
|
506
|
+
elsewhere), tags and fixture keys.
|
|
507
|
+
|
|
508
|
+
Example: evalkit datasets list · evalkit datasets list finance@dev --state approved
|
|
509
|
+
· evalkit datasets list finance --tag statement-analysis
|
|
510
|
+
"""
|
|
511
|
+
if dataset is None:
|
|
512
|
+
if state is not None or tag is not None:
|
|
513
|
+
raise ApiError("--state and --tag filter a dataset's cases; pass a dataset")
|
|
514
|
+
for ds in _client().get("/api/datasets").json():
|
|
515
|
+
last = ds.get("last_sync") or {}
|
|
516
|
+
sha = (last.get("detail") or {}).get("sha") or "-"
|
|
517
|
+
typer.echo(f"{ds['ref']:<32} kind={ds['kind']:<10} sha={sha[:12]}")
|
|
518
|
+
return
|
|
519
|
+
dataset_slug = slug(_parse(dataset, _connection().username or ""))
|
|
520
|
+
params = {"tag": tag} if tag is not None else {}
|
|
521
|
+
rows = _client().get(f"/api/datasets/{encode(dataset_slug)}/cases", params=params).json()
|
|
522
|
+
if state is not None and rows and all(row["review"] is None for row in rows):
|
|
523
|
+
raise ApiError(f"{dataset_slug} has no review state; only suites in shared workspaces do")
|
|
524
|
+
shown = [row for row in rows if state is None or (row["review"] or {}).get("state") == state]
|
|
525
|
+
for row in shown:
|
|
526
|
+
review = row["review"] or {}
|
|
527
|
+
fixtures = [f["key"] for f in row["files"]]
|
|
528
|
+
typer.echo(
|
|
529
|
+
f"{row['case_id']:<40} {review.get('state', '-'):<18} "
|
|
530
|
+
f"tags={','.join(row['split_tags']) or '-'} "
|
|
531
|
+
f"fixtures={','.join(fixtures) or '-'}"
|
|
532
|
+
)
|
|
533
|
+
typer.echo(f"{len(shown)} case(s) in {dataset_slug}")
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class _Progress:
|
|
537
|
+
def __init__(self) -> None:
|
|
538
|
+
self.exp_id: int | None = None
|
|
539
|
+
|
|
540
|
+
def __call__(self, line: str) -> None:
|
|
541
|
+
if line.startswith("experiment "):
|
|
542
|
+
self.exp_id = int(line.split()[1])
|
|
543
|
+
typer.echo(line)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _warn_dropped(dropped: list[str]) -> None:
|
|
547
|
+
if dropped:
|
|
548
|
+
typer.secho(dropped_cases_text(dropped), fg="yellow")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _run_on_server(
|
|
552
|
+
dataset_slug: str,
|
|
553
|
+
agent_slug: str,
|
|
554
|
+
selection: Selection,
|
|
555
|
+
repetitions: int,
|
|
556
|
+
model: str | None,
|
|
557
|
+
resume: int | None,
|
|
558
|
+
revision: GitRevision,
|
|
559
|
+
judge: dict | None,
|
|
560
|
+
) -> int:
|
|
561
|
+
client = _client()
|
|
562
|
+
if resume:
|
|
563
|
+
exp_id = int(client.post(f"/api/experiments/{resume}/resume").json()["id"])
|
|
564
|
+
else:
|
|
565
|
+
payload = {
|
|
566
|
+
"dataset": dataset_slug,
|
|
567
|
+
"agent": agent_slug,
|
|
568
|
+
"repetitions": repetitions,
|
|
569
|
+
"selection": selection.model_dump(exclude_none=True),
|
|
570
|
+
"model": model,
|
|
571
|
+
"cases": revision.model_dump(exclude_none=True) or None,
|
|
572
|
+
"evalkit_version": version.current(),
|
|
573
|
+
"judge": judge,
|
|
574
|
+
}
|
|
575
|
+
created = client.post("/api/experiments", json=payload).json()
|
|
576
|
+
exp_id = int(created["id"])
|
|
577
|
+
_warn_dropped(created.get("dropped_cases") or [])
|
|
578
|
+
typer.echo(f"experiment {exp_id}: {_experiment_url(exp_id)}")
|
|
579
|
+
last = None
|
|
580
|
+
while True:
|
|
581
|
+
body = client.get(f"/api/experiments/{exp_id}").json()
|
|
582
|
+
if body["status"] != last:
|
|
583
|
+
typer.echo(f" {body['status']} ({_progress_text(body['progress'])})")
|
|
584
|
+
last = body["status"]
|
|
585
|
+
if body["status"] in {"completed", "failed"}:
|
|
586
|
+
break
|
|
587
|
+
time.sleep(RUN_POLL_SECONDS)
|
|
588
|
+
if body["status"] == "failed":
|
|
589
|
+
typer.secho(f"experiment failed: {body.get('status_error')}", fg="red")
|
|
590
|
+
raise typer.Exit(code=1)
|
|
591
|
+
return exp_id
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@app.command()
|
|
595
|
+
def run(
|
|
596
|
+
dataset: str = typer.Argument(
|
|
597
|
+
...,
|
|
598
|
+
help="finance · finance@dev · finance/<split> · finance/<file>.yaml · a path",
|
|
599
|
+
),
|
|
600
|
+
agent: str = typer.Option(
|
|
601
|
+
...,
|
|
602
|
+
"--agent",
|
|
603
|
+
help="myagent (yours, your laptop drives it) or ouragent@dev (the server drives it).",
|
|
604
|
+
),
|
|
605
|
+
runs: int = typer.Option(1, "--runs", help="Repetitions per case."),
|
|
606
|
+
case_id: list[str] = typer.Option(None, "--case-id", help="Run only these case ids."),
|
|
607
|
+
model: str = typer.Option(None, "--model", help="Model override (if the harness accepts it)."),
|
|
608
|
+
resume: int = typer.Option(None, "--resume", help="Resume an experiment by id."),
|
|
609
|
+
judge_model: str | None = typer.Option(
|
|
610
|
+
None, "--judge-model", help="Judge model for this experiment; default: the server's."
|
|
611
|
+
),
|
|
612
|
+
judge_base_url: str | None = typer.Option(
|
|
613
|
+
None, "--judge-base-url", help="Judge endpoint; its key is credentials.judges[url]."
|
|
614
|
+
),
|
|
615
|
+
judge_effort: str | None = typer.Option(None, "--judge-effort", help="Judge reasoning effort."),
|
|
616
|
+
):
|
|
617
|
+
"""Run an experiment. Your own agent runs from this machine; results land on the server.
|
|
618
|
+
|
|
619
|
+
Runs every selected case, whatever its review state. Checks every selected
|
|
620
|
+
case's input against the agent's input_schema (or, when it declares none,
|
|
621
|
+
the default: a required string `question`) and refuses the whole experiment
|
|
622
|
+
if any case fails, listing each one. `evalkit lint <dir> --agent <name>`
|
|
623
|
+
shows the same errors before you sync.
|
|
624
|
+
|
|
625
|
+
Example: evalkit run finance --agent research-agent ·
|
|
626
|
+
evalkit run finance@dev/statement-analysis --agent ouragent@dev
|
|
627
|
+
"""
|
|
628
|
+
conn = _connection()
|
|
629
|
+
me = conn.username or ""
|
|
630
|
+
agent_ref = _parse(agent, me)
|
|
631
|
+
definition = _agent_definition(agent_ref)
|
|
632
|
+
route = driver(definition)
|
|
633
|
+
if route is Driver.SDK:
|
|
634
|
+
raise ApiError(
|
|
635
|
+
f"{definition.ref} is a code-driven agent (no harness_url); "
|
|
636
|
+
"run it with evalkit.sdk.run_experiment, or if it serves the Harness API, "
|
|
637
|
+
f"set its URL: evalkit agents edit {definition.ref} --harness-url URL"
|
|
638
|
+
)
|
|
639
|
+
if route is Driver.CLI and definition.workspace != me:
|
|
640
|
+
raise ApiError(
|
|
641
|
+
f"{definition.ref} belongs to {definition.workspace}'s workspace; "
|
|
642
|
+
"only its owner can run it"
|
|
643
|
+
)
|
|
644
|
+
local = _find_local()
|
|
645
|
+
if local is not None and route is Driver.CLI:
|
|
646
|
+
local = _with_secrets(local, scoring=True)
|
|
647
|
+
_require_agent_key(local, conn, definition)
|
|
648
|
+
resolved = _resolve(dataset, local.cases if local else None, me)
|
|
649
|
+
selection = Selection(case_ids=list(case_id)) if case_id else resolved.selection
|
|
650
|
+
revision = git_revision(resolved.dataset_dir) if resolved.dataset_dir else GitRevision()
|
|
651
|
+
if resolved.dataset_dir is not None and resume is None:
|
|
652
|
+
_sync_and_report(
|
|
653
|
+
resolved.dataset_dir,
|
|
654
|
+
name=resolved.ref.name,
|
|
655
|
+
workspace=me,
|
|
656
|
+
revision=revision,
|
|
657
|
+
fixtures_dir=local.fixtures if local else None,
|
|
658
|
+
)
|
|
659
|
+
dataset_slug = slug(resolved.ref)
|
|
660
|
+
judge = _judge_override(judge_model, judge_base_url, judge_effort)
|
|
661
|
+
if route is Driver.SERVER:
|
|
662
|
+
exp_id = _run_on_server(
|
|
663
|
+
dataset_slug,
|
|
664
|
+
slug(agent_ref),
|
|
665
|
+
selection,
|
|
666
|
+
runs,
|
|
667
|
+
model,
|
|
668
|
+
resume,
|
|
669
|
+
revision,
|
|
670
|
+
judge,
|
|
671
|
+
)
|
|
672
|
+
else:
|
|
673
|
+
if local is None:
|
|
674
|
+
raise ApiError("no evalkit.yaml found above the current directory")
|
|
675
|
+
progress = _Progress()
|
|
676
|
+
try:
|
|
677
|
+
exp_id = asyncio.run(
|
|
678
|
+
run_local(
|
|
679
|
+
conn,
|
|
680
|
+
local,
|
|
681
|
+
agent=agent_ref,
|
|
682
|
+
dataset_slug=dataset_slug,
|
|
683
|
+
selection=selection,
|
|
684
|
+
repetitions=runs,
|
|
685
|
+
model=model,
|
|
686
|
+
resume=resume,
|
|
687
|
+
revision=revision,
|
|
688
|
+
progress=progress,
|
|
689
|
+
judge=JudgeSettings.model_validate(judge) if judge else None,
|
|
690
|
+
)
|
|
691
|
+
)
|
|
692
|
+
except KeyboardInterrupt:
|
|
693
|
+
if progress.exp_id is not None:
|
|
694
|
+
asyncio.run(mark_interrupted(conn, progress.exp_id, me))
|
|
695
|
+
typer.secho("interrupted", fg="red")
|
|
696
|
+
raise typer.Exit(code=130) from None
|
|
697
|
+
except ScoringFailed as exc:
|
|
698
|
+
raise ApiError(f"{exc}\nfinish it with: evalkit evaluate {exc.exp_id}") from exc
|
|
699
|
+
except (VersionMismatch, ApiRequestError, ValueError, KeyError) as exc:
|
|
700
|
+
raise ApiError(str(exc)) from exc
|
|
701
|
+
_render_summary(_client().get(f"/api/experiments/{exp_id}/scorecard").json())
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _judge_override(model: str | None, base_url: str | None, effort: str | None) -> dict | None:
|
|
705
|
+
changes = {
|
|
706
|
+
key: value
|
|
707
|
+
for key, value in {
|
|
708
|
+
"model": model,
|
|
709
|
+
"base_url": base_url,
|
|
710
|
+
"reasoning_effort": effort,
|
|
711
|
+
}.items()
|
|
712
|
+
if value is not None
|
|
713
|
+
}
|
|
714
|
+
if not changes:
|
|
715
|
+
return None
|
|
716
|
+
return {**_client().get("/api/judge-default").json(), **changes}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _require_agent_key(
|
|
720
|
+
local: LocalConfig, conn: connection.Connection, agent: AgentDefinition
|
|
721
|
+
) -> None:
|
|
722
|
+
try:
|
|
723
|
+
local_credentials(local, conn).key_for("agent", agent.ref)
|
|
724
|
+
except MissingCredential as exc:
|
|
725
|
+
raise ApiError(str(exc)) from exc
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _agent_definition(ref: Reference) -> AgentDefinition:
|
|
729
|
+
return definition_of(_client().get(f"/api/agents/{encode(explicit(ref))}").json())
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
@app.command()
|
|
733
|
+
def evaluate(
|
|
734
|
+
experiment_id: int,
|
|
735
|
+
refetch_trajectories: bool = typer.Option(
|
|
736
|
+
False,
|
|
737
|
+
"--refetch-trajectories",
|
|
738
|
+
help="Poll the collector again for runs whose trajectory was partial or unavailable, "
|
|
739
|
+
"then re-score them.",
|
|
740
|
+
),
|
|
741
|
+
):
|
|
742
|
+
"""Re-score an experiment's recorded runs from this machine (idempotent).
|
|
743
|
+
|
|
744
|
+
Scores already recorded are kept; evaluators that errored run again.
|
|
745
|
+
|
|
746
|
+
Refuses experiments that used Python evaluators; re-score those with
|
|
747
|
+
evalkit.sdk.evaluate_experiment from the script that defines them.
|
|
748
|
+
|
|
749
|
+
Example: evalkit evaluate <experiment-id> --refetch-trajectories
|
|
750
|
+
"""
|
|
751
|
+
local = _with_secrets(_local_config(), scoring=True)
|
|
752
|
+
conn = _connection()
|
|
753
|
+
body = _client().get(f"/api/experiments/{experiment_id}").json()
|
|
754
|
+
python_evaluators = body["config"]["snapshot"].get("code_evaluators") or []
|
|
755
|
+
if python_evaluators:
|
|
756
|
+
raise ApiError(
|
|
757
|
+
f"experiment {experiment_id} used python evaluators "
|
|
758
|
+
f"{', '.join(python_evaluators)}; re-score it with evalkit.sdk.evaluate_experiment "
|
|
759
|
+
"from the script that defines them"
|
|
760
|
+
)
|
|
761
|
+
try:
|
|
762
|
+
asyncio.run(
|
|
763
|
+
evaluate_local(
|
|
764
|
+
conn,
|
|
765
|
+
local,
|
|
766
|
+
experiment_id,
|
|
767
|
+
progress=typer.echo,
|
|
768
|
+
refetch_trajectories=refetch_trajectories,
|
|
769
|
+
)
|
|
770
|
+
)
|
|
771
|
+
except ScoringFailed as exc:
|
|
772
|
+
raise ApiError(f"{exc}\nfinish it with: evalkit evaluate {experiment_id}") from exc
|
|
773
|
+
except (VersionMismatch, ApiRequestError, ValueError, KeyError) as exc:
|
|
774
|
+
raise ApiError(str(exc)) from exc
|
|
775
|
+
typer.echo(f"evaluated {experiment_id}")
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _progress_text(progress: dict) -> str:
|
|
779
|
+
total = progress["total"]
|
|
780
|
+
return f"{progress['ran']}/{total} ran, {progress['evaluated']}/{total} evaluated"
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _experiment_url(exp_id: int) -> str:
|
|
784
|
+
return f"{_connection().url.rstrip('/')}/experiments/{exp_id}"
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _lease_text(body: dict) -> str:
|
|
788
|
+
if body["status"] not in {"running", "evaluating"} or not body.get("lease_holder"):
|
|
789
|
+
return ""
|
|
790
|
+
expires = datetime.fromisoformat(body["lease_expires_at"])
|
|
791
|
+
left = int((expires - datetime.now(UTC)).total_seconds())
|
|
792
|
+
if left < 0:
|
|
793
|
+
return f"\n held by {body['lease_holder']} (lease expired {-left}s ago; runner stopped?)"
|
|
794
|
+
return f"\n held by {body['lease_holder']} (lease expires in {left}s)"
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
@app.command()
|
|
798
|
+
def status(experiment_id: int):
|
|
799
|
+
"""Show an experiment's status, progress and, while it runs, who holds it.
|
|
800
|
+
|
|
801
|
+
Example: evalkit status 42
|
|
802
|
+
"""
|
|
803
|
+
body = _client().get(f"/api/experiments/{experiment_id}").json()
|
|
804
|
+
typer.echo(
|
|
805
|
+
f"experiment {body['id']} #{body['number']} of {body['dataset']} agent={body['agent']}\n"
|
|
806
|
+
f" status={body['status']} {_progress_text(body['progress'])}{_lease_text(body)}"
|
|
807
|
+
)
|
|
808
|
+
if body.get("status_error"):
|
|
809
|
+
typer.secho(f" error: {body['status_error']}", fg="red")
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _render_summary(report: dict) -> None:
|
|
813
|
+
for line in summary_lines(report):
|
|
814
|
+
typer.echo(line)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@app.command()
|
|
818
|
+
def summary(
|
|
819
|
+
experiment_id: int,
|
|
820
|
+
baseline: int = typer.Option(None, "--baseline", help="Baseline experiment id for deltas."),
|
|
821
|
+
):
|
|
822
|
+
"""Render an experiment's scorecard as a console table.
|
|
823
|
+
|
|
824
|
+
Example: evalkit summary <experiment-id> --baseline <older-id>
|
|
825
|
+
"""
|
|
826
|
+
params = {"baseline_id": baseline} if baseline else {}
|
|
827
|
+
_render_summary(
|
|
828
|
+
_client().get(f"/api/experiments/{experiment_id}/scorecard", params=params).json()
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _require_own_agent(definition: AgentDefinition, me: str, action: str) -> None:
|
|
833
|
+
if not definition.workspace_is_shared and definition.workspace != me:
|
|
834
|
+
raise ApiError(
|
|
835
|
+
f"{definition.ref} belongs to {definition.workspace}'s workspace; "
|
|
836
|
+
f"only its owner can {action} it"
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@harness_app.command("check")
|
|
841
|
+
def harness_check(
|
|
842
|
+
agent: str = typer.Argument(
|
|
843
|
+
..., help="myagent (yours, your laptop drives it) or ouragent@dev (the server drives it)."
|
|
844
|
+
),
|
|
845
|
+
):
|
|
846
|
+
"""Exercise a harness's Harness API and print a per-capability table.
|
|
847
|
+
|
|
848
|
+
Example: evalkit harness check myagent · evalkit harness check ouragent@dev
|
|
849
|
+
"""
|
|
850
|
+
conn = _connection()
|
|
851
|
+
ref = _parse(agent, conn.username or "")
|
|
852
|
+
definition = _agent_definition(ref)
|
|
853
|
+
_require_own_agent(definition, conn.username or "", "check")
|
|
854
|
+
if definition.archived:
|
|
855
|
+
raise ApiError(f"agent {definition.ref} is archived")
|
|
856
|
+
if driver(definition) is Driver.SDK:
|
|
857
|
+
raise ApiError(
|
|
858
|
+
f"{definition.ref} is a code-driven agent (no harness_url); "
|
|
859
|
+
"use evalkit traces check AGENT SESSION_ID"
|
|
860
|
+
)
|
|
861
|
+
if server_may_drive(definition):
|
|
862
|
+
checks = _client().post(f"/api/agents/{encode(slug(ref))}/check").json()["checks"]
|
|
863
|
+
else:
|
|
864
|
+
local = _with_secrets(_local_config(), scoring=False)
|
|
865
|
+
try:
|
|
866
|
+
checks = asyncio.run(check_local(conn, local, ref))
|
|
867
|
+
except (ValueError, KeyError, ApiRequestError) as exc:
|
|
868
|
+
raise ApiError(str(exc)) from exc
|
|
869
|
+
failed = False
|
|
870
|
+
for capability, entry in checks.items():
|
|
871
|
+
marker = {"pass": "✓", "warn": "!", "fail": "✗", "skipped": "-"}[entry["status"]]
|
|
872
|
+
typer.echo(f" {marker} {capability:<14} {entry['detail']}")
|
|
873
|
+
failed = failed or entry["status"] == "fail"
|
|
874
|
+
if failed:
|
|
875
|
+
raise typer.Exit(code=1)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
@users_app.command("add")
|
|
879
|
+
def users_add(username: str, password: str = typer.Option(..., prompt=True, hide_input=True)):
|
|
880
|
+
"""Create a user (admin only). Example: evalkit users add alice"""
|
|
881
|
+
_client().post("/api/users", json={"username": username, "password": password})
|
|
882
|
+
typer.echo(f"created {username}")
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@users_app.command("archive")
|
|
886
|
+
def users_archive(username: str):
|
|
887
|
+
"""Archive a user: revoke their login tokens, block login, archive their workspace.
|
|
888
|
+
|
|
889
|
+
Agent tokens they minted keep working: they belong to the agent.
|
|
890
|
+
|
|
891
|
+
Users are never deleted, so their experiments keep an owner and the name
|
|
892
|
+
stays reserved. Admin only. Example: evalkit users archive alice
|
|
893
|
+
"""
|
|
894
|
+
_client().post(f"/api/users/{username}/archive")
|
|
895
|
+
typer.echo(f"archived {username}")
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
@users_app.command("restore")
|
|
899
|
+
def users_restore(username: str):
|
|
900
|
+
"""Let an archived user log in again (admin only). Example: evalkit users restore alice
|
|
901
|
+
|
|
902
|
+
Their personal workspace is restored too when it was archived with them.
|
|
903
|
+
"""
|
|
904
|
+
_client().post(f"/api/users/{username}/restore")
|
|
905
|
+
typer.echo(f"restored {username}")
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@users_app.command("set-password")
|
|
909
|
+
def users_set_password(
|
|
910
|
+
username: str,
|
|
911
|
+
password: str = typer.Option(..., prompt=True, hide_input=True, confirmation_prompt=True),
|
|
912
|
+
):
|
|
913
|
+
"""Set a user's password and sign them out everywhere (admin only).
|
|
914
|
+
|
|
915
|
+
Example: evalkit users set-password alice
|
|
916
|
+
"""
|
|
917
|
+
_client().post(f"/api/users/{username}/password", json={"password": password})
|
|
918
|
+
typer.echo(f"password set for {username}")
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
@users_app.command("promote")
|
|
922
|
+
def users_promote(username: str):
|
|
923
|
+
"""Make a user an admin (admin only). Example: evalkit users promote alice"""
|
|
924
|
+
_client().post(f"/api/users/{username}/promote")
|
|
925
|
+
typer.echo(f"{username} is an admin")
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
@users_app.command("demote")
|
|
929
|
+
def users_demote(username: str):
|
|
930
|
+
"""Remove a user's admin flag; the last admin stays. Example: evalkit users demote alice"""
|
|
931
|
+
_client().post(f"/api/users/{username}/demote")
|
|
932
|
+
typer.echo(f"{username} is no longer an admin")
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
@users_app.command("list")
|
|
936
|
+
def users_list():
|
|
937
|
+
"""List users (admin only). Example: evalkit users list"""
|
|
938
|
+
for user in _client().get("/api/users").json():
|
|
939
|
+
flags = " ".join(
|
|
940
|
+
flag for flag, on in (("ADMIN", user["is_admin"]), ("ARCHIVED", user["archived"])) if on
|
|
941
|
+
)
|
|
942
|
+
typer.echo(f"{user['username']:<24} {flags:<16} created {user['created_at']}")
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
DOCS_DIR = "evalkit-docs"
|
|
946
|
+
INSTALL_LEAVES_OUT = shutil.ignore_patterns(".env", "*.whl", "__pycache__", "*.pyc")
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
@skills_app.command("install")
|
|
950
|
+
def skills_install(
|
|
951
|
+
target: Path = typer.Option(
|
|
952
|
+
None,
|
|
953
|
+
"--target",
|
|
954
|
+
help="Skills dir; default .claude/skills/ if .claude/ exists, else .agents/skills/",
|
|
955
|
+
),
|
|
956
|
+
):
|
|
957
|
+
"""Copy the bundled skills, and the user docs they link to, into this repo's agent-skills
|
|
958
|
+
directory.
|
|
959
|
+
|
|
960
|
+
The docs go to evalkit-docs/ beside the skills. A directory an earlier install put there
|
|
961
|
+
that this version no longer ships is removed; other directories are left alone.
|
|
962
|
+
`evalkit update` refreshes every directory installed this way.
|
|
963
|
+
|
|
964
|
+
Example: evalkit skills install
|
|
965
|
+
"""
|
|
966
|
+
root = skills_root()
|
|
967
|
+
skills = sorted(p for p in root.iterdir() if (p / "SKILL.md").is_file())
|
|
968
|
+
if not skills:
|
|
969
|
+
raise ApiError(f"no skills found under {root}")
|
|
970
|
+
if target is None:
|
|
971
|
+
target = Path(".claude/skills") if Path(".claude").is_dir() else Path(".agents/skills")
|
|
972
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
973
|
+
sources = {skill.name: skill for skill in skills} | {DOCS_DIR: docs_root()}
|
|
974
|
+
manifest = target / INSTALL_MANIFEST
|
|
975
|
+
previous = manifest.read_text().split() if manifest.is_file() else []
|
|
976
|
+
for name in previous:
|
|
977
|
+
if name not in sources and (target / name).is_dir():
|
|
978
|
+
shutil.rmtree(target / name)
|
|
979
|
+
typer.echo(f"removed {name} (no longer shipped)")
|
|
980
|
+
for name, source in sources.items():
|
|
981
|
+
destination = target / name
|
|
982
|
+
if destination.exists():
|
|
983
|
+
shutil.rmtree(destination)
|
|
984
|
+
shutil.copytree(source, destination, ignore=INSTALL_LEAVES_OUT)
|
|
985
|
+
typer.echo(f"installed {name} → {destination}")
|
|
986
|
+
manifest.write_text("".join(f"{name}\n" for name in sources))
|
|
987
|
+
record_skill_target(target)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
@contracts_app.command("export")
|
|
991
|
+
def contracts_export(
|
|
992
|
+
target: Path = typer.Option(
|
|
993
|
+
Path("contracts"), "--target", help="Directory to write the contract files into."
|
|
994
|
+
),
|
|
995
|
+
):
|
|
996
|
+
"""Write the bundled API contracts (OpenAPI + accepted ATIF versions) to a directory.
|
|
997
|
+
|
|
998
|
+
Exports harness-api-v1.json, custom-evaluator-api-v1.json and
|
|
999
|
+
atif-accepted.json, matching the installed evalkit version.
|
|
1000
|
+
|
|
1001
|
+
Example: evalkit contracts export --target ./contracts
|
|
1002
|
+
"""
|
|
1003
|
+
root = contracts_root()
|
|
1004
|
+
files = sorted(p for p in root.iterdir() if p.suffix == ".json")
|
|
1005
|
+
if not files:
|
|
1006
|
+
raise ApiError(f"no contract files found under {root}")
|
|
1007
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
1008
|
+
for file in files:
|
|
1009
|
+
shutil.copyfile(file, target / file.name)
|
|
1010
|
+
typer.echo(f"exported {file.name} → {target / file.name}")
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
@app.command("init-server")
|
|
1014
|
+
def init_server_command(
|
|
1015
|
+
directory: Path = typer.Option(
|
|
1016
|
+
init_server.DEFAULT_DIR, "--dir", help="Directory for the try kit, config.yaml and .env."
|
|
1017
|
+
),
|
|
1018
|
+
database_url: str | None = typer.Option(
|
|
1019
|
+
None,
|
|
1020
|
+
"--database-url",
|
|
1021
|
+
envvar="EVALKIT_DATABASE_URL",
|
|
1022
|
+
help="Postgres URL; the database is created when missing.",
|
|
1023
|
+
),
|
|
1024
|
+
llm_base_url: str | None = typer.Option(
|
|
1025
|
+
None,
|
|
1026
|
+
"--llm-base-url",
|
|
1027
|
+
envvar="LLM_BASE_URL",
|
|
1028
|
+
help="OpenAI-compatible endpoint for the judge and the sample agent.",
|
|
1029
|
+
),
|
|
1030
|
+
llm_model: str | None = typer.Option(
|
|
1031
|
+
None, "--llm-model", envvar="LLM_MODEL", help="Model name at that endpoint."
|
|
1032
|
+
),
|
|
1033
|
+
llm_api_key: str | None = typer.Option(
|
|
1034
|
+
None, "--llm-api-key", envvar="LLM_API_KEY", help="API key for that endpoint."
|
|
1035
|
+
),
|
|
1036
|
+
admin_password: str | None = typer.Option(
|
|
1037
|
+
None,
|
|
1038
|
+
"--admin-password",
|
|
1039
|
+
envvar="EVALKIT_ADMIN_PASSWORD",
|
|
1040
|
+
help="Password of the `admin` user you log in as.",
|
|
1041
|
+
),
|
|
1042
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Never prompt; use flags, env, defaults."),
|
|
1043
|
+
force: bool = typer.Option(
|
|
1044
|
+
False,
|
|
1045
|
+
"--force",
|
|
1046
|
+
help="Overwrite an existing kit in --dir: config.yaml, .env and the kit's files, "
|
|
1047
|
+
"including your edits to its cases and scripts.",
|
|
1048
|
+
),
|
|
1049
|
+
kit_only: bool = typer.Option(
|
|
1050
|
+
False, "--kit-only", help="Only copy the try kit; no database, config.yaml or .env."
|
|
1051
|
+
),
|
|
1052
|
+
):
|
|
1053
|
+
"""Set up an evalkit server on this machine, against a Postgres you already run.
|
|
1054
|
+
|
|
1055
|
+
Copies the try kit (sample agent, cases, run.py) into --dir.
|
|
1056
|
+
|
|
1057
|
+
Creates the database when missing and migrates it.
|
|
1058
|
+
|
|
1059
|
+
Writes config.yaml (blobs under <dir>/data) and .env (your LLM, the admin password).
|
|
1060
|
+
|
|
1061
|
+
Each value comes from its flag or env var; anything missing is prompted for.
|
|
1062
|
+
|
|
1063
|
+
Example: evalkit init-server · evalkit init-server --kit-only --dir ./evalkit-try
|
|
1064
|
+
"""
|
|
1065
|
+
existing = init_server.kit_files_in(directory)
|
|
1066
|
+
if existing and not force:
|
|
1067
|
+
raise ApiError(
|
|
1068
|
+
f"{directory} already holds a try kit ({', '.join(existing)}); "
|
|
1069
|
+
"pass --force to overwrite it, edits included"
|
|
1070
|
+
)
|
|
1071
|
+
if kit_only:
|
|
1072
|
+
init_server.copy_kit(directory, docker=True)
|
|
1073
|
+
typer.echo(f"copied the try kit to {directory}")
|
|
1074
|
+
return
|
|
1075
|
+
if server_commands is None:
|
|
1076
|
+
raise ApiError(
|
|
1077
|
+
"`evalkit init-server` needs the server extra — pip install 'evalkit-py[server]'"
|
|
1078
|
+
)
|
|
1079
|
+
flags = {
|
|
1080
|
+
"database_url": database_url,
|
|
1081
|
+
"llm_base_url": llm_base_url,
|
|
1082
|
+
"llm_model": llm_model,
|
|
1083
|
+
"llm_api_key": llm_api_key,
|
|
1084
|
+
"admin_password": admin_password,
|
|
1085
|
+
}
|
|
1086
|
+
answers = init_server.resolve(flags, yes=yes)
|
|
1087
|
+
typer.echo(init_server.DATABASE_NOTICE)
|
|
1088
|
+
created = server_commands.init_database(answers.database_url)
|
|
1089
|
+
typer.echo(
|
|
1090
|
+
"created the database and migrated it"
|
|
1091
|
+
if created
|
|
1092
|
+
else "the database already existed; migrated it"
|
|
1093
|
+
)
|
|
1094
|
+
saved_key = init_server.existing_encryption_key(directory)
|
|
1095
|
+
new_key = saved_key or server_commands.new_encryption_key()
|
|
1096
|
+
encryption_key: str | None = new_key
|
|
1097
|
+
if created or not server_commands.has_llm_setup(answers.database_url):
|
|
1098
|
+
server_commands.register_default_judge(
|
|
1099
|
+
answers.database_url,
|
|
1100
|
+
new_key,
|
|
1101
|
+
answers.llm_base_url,
|
|
1102
|
+
answers.llm_api_key,
|
|
1103
|
+
answers.llm_model,
|
|
1104
|
+
)
|
|
1105
|
+
typer.echo(f"registered {answers.llm_base_url} as the default judge ({answers.llm_model})")
|
|
1106
|
+
else:
|
|
1107
|
+
typer.secho(
|
|
1108
|
+
"the database already has LLM providers or a default judge; left them as they are. "
|
|
1109
|
+
"Change them with `evalkit llm-providers` and `evalkit judge-default set`. A server "
|
|
1110
|
+
"using this database needs the encryption_key it was set up with.",
|
|
1111
|
+
fg="yellow",
|
|
1112
|
+
)
|
|
1113
|
+
if saved_key is None:
|
|
1114
|
+
encryption_key = None
|
|
1115
|
+
typer.secho(
|
|
1116
|
+
f"set EVALKIT_ENCRYPTION_KEY in {directory / '.env'} to that key before "
|
|
1117
|
+
"`evalkit serve`; the line is there, commented out.",
|
|
1118
|
+
fg="yellow",
|
|
1119
|
+
)
|
|
1120
|
+
init_server.copy_kit(directory, docker=False)
|
|
1121
|
+
init_server.write_server_files(directory, answers, encryption_key)
|
|
1122
|
+
typer.echo(f"set up {directory}: config.yaml, .env and the try kit")
|
|
1123
|
+
typer.echo("next, start the server:")
|
|
1124
|
+
typer.echo(f" cd {directory} && evalkit serve")
|
|
1125
|
+
typer.echo("then, in another terminal from the same folder, log in:")
|
|
1126
|
+
typer.echo(" evalkit login --url http://localhost:8020 --username admin")
|
|
1127
|
+
typer.echo(" (this replaces a login you saved; Try it locally step 1 shows how to keep it)")
|
|
1128
|
+
typer.echo("and follow Try it locally from step 2 (the CLI is already installed)")
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
@app.command()
|
|
1132
|
+
def serve(
|
|
1133
|
+
config: Path = typer.Option(Path("config.yaml"), "--config", help="Path to config.yaml."),
|
|
1134
|
+
host: str = typer.Option("127.0.0.1", "--host"),
|
|
1135
|
+
port: int = typer.Option(8020, "--port"),
|
|
1136
|
+
):
|
|
1137
|
+
"""Start the evalkit server (API + UI).
|
|
1138
|
+
|
|
1139
|
+
Example: evalkit serve --config /etc/evalkit/config.yaml
|
|
1140
|
+
"""
|
|
1141
|
+
if server_commands is None:
|
|
1142
|
+
raise ApiError("`evalkit serve` needs the server extra — pip install 'evalkit-py[server]'")
|
|
1143
|
+
server_commands.serve(_server_config(config), host, port)
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
@db_app.command("upgrade")
|
|
1147
|
+
def db_upgrade(
|
|
1148
|
+
config: Path = typer.Option(Path("config.yaml"), "--config", help="Path to config.yaml."),
|
|
1149
|
+
revision: str = typer.Option("head", "--revision", help="Stop at this revision, e.g. 0006."),
|
|
1150
|
+
):
|
|
1151
|
+
"""Run schema migrations against the configured database.
|
|
1152
|
+
|
|
1153
|
+
Example: evalkit db upgrade --config /etc/evalkit/config.yaml --revision 0006
|
|
1154
|
+
"""
|
|
1155
|
+
if server_commands is None:
|
|
1156
|
+
raise ApiError(
|
|
1157
|
+
"`evalkit db upgrade` needs the server extra — pip install 'evalkit-py[server]'"
|
|
1158
|
+
)
|
|
1159
|
+
server_commands.db_upgrade(_server_config(config), revision)
|
|
1160
|
+
typer.echo(f"migrations applied up to {revision}")
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
@db_app.command("rekey")
|
|
1164
|
+
def db_rekey(
|
|
1165
|
+
config: Path = typer.Option(Path("config.yaml"), "--config", help="Path to config.yaml."),
|
|
1166
|
+
old_key_env: str = typer.Option(..., "--old-key-env", help="Env var with the old key."),
|
|
1167
|
+
new_key_env: str = typer.Option(..., "--new-key-env", help="Env var with the new key."),
|
|
1168
|
+
):
|
|
1169
|
+
"""Re-encrypt every stored API key (agents, remote evaluators, LLM providers, trace
|
|
1170
|
+
sources) after an encryption_key change (server-side).
|
|
1171
|
+
|
|
1172
|
+
Example: evalkit db rekey --config config.yaml --old-key-env OLD --new-key-env NEW
|
|
1173
|
+
"""
|
|
1174
|
+
if server_commands is None:
|
|
1175
|
+
raise ApiError("`db rekey` needs the server extra — pip install 'evalkit-py[server]'")
|
|
1176
|
+
missing = [name for name in (old_key_env, new_key_env) if name not in os.environ]
|
|
1177
|
+
if missing:
|
|
1178
|
+
raise ApiError(f"set {', '.join(missing)} first")
|
|
1179
|
+
count = asyncio.run(
|
|
1180
|
+
server_commands.rekey(
|
|
1181
|
+
_server_config(config), os.environ[old_key_env], os.environ[new_key_env]
|
|
1182
|
+
)
|
|
1183
|
+
)
|
|
1184
|
+
typer.echo(f"re-encrypted {count} stored API key(s)")
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
if __name__ == "__main__":
|
|
1188
|
+
app()
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def _selection(tag: str | None, case_id: list[str] | None) -> dict:
|
|
1192
|
+
if case_id:
|
|
1193
|
+
return {"case_ids": list(case_id)}
|
|
1194
|
+
if tag:
|
|
1195
|
+
return {"tag": tag}
|
|
1196
|
+
return {"all": True}
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _dataset_ref(dataset: str) -> str:
|
|
1200
|
+
return encode(slug(_parse(dataset, _connection().username or "")))
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _case_path(dataset: str, case_id: str) -> str:
|
|
1204
|
+
return f"/api/datasets/{_dataset_ref(dataset)}/cases/{case_id}"
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def _fmt_comment(comment: dict, *, indent: int) -> str:
|
|
1208
|
+
flag = " [resolved]" if comment["resolved"] else ""
|
|
1209
|
+
pad = " " * indent
|
|
1210
|
+
head = f"{pad}#{comment['id']} {comment['author']} · {comment['created_at']}{flag}"
|
|
1211
|
+
return f"{head}\n{pad}{comment['body']}"
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _post_decision(dataset: str, case_id: str, action: str, comment: str | None) -> None:
|
|
1215
|
+
body = {"action": action, "comment": comment}
|
|
1216
|
+
result = _client().post(f"{_case_path(dataset, case_id)}/review", json=body).json()
|
|
1217
|
+
typer.echo(f"{case_id}: {result['state']}")
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
@review_app.command("request")
|
|
1221
|
+
def review_request(
|
|
1222
|
+
dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
|
|
1223
|
+
reviewer: str = typer.Option(..., "--reviewer", help="Reviewer username."),
|
|
1224
|
+
tag: str = typer.Option(None, "--tag", help="Only cases carrying this tag."),
|
|
1225
|
+
case_id: list[str] = typer.Option(None, "--case-id", help="Only these case ids."),
|
|
1226
|
+
):
|
|
1227
|
+
"""Ask a reviewer to look at cases (all cases when no selector is given).
|
|
1228
|
+
|
|
1229
|
+
Example: evalkit review request finance --case-id mf-industry-07 --reviewer sme@client
|
|
1230
|
+
"""
|
|
1231
|
+
body = {"selection": _selection(tag, case_id), "reviewer": reviewer}
|
|
1232
|
+
requested = (
|
|
1233
|
+
_client().post(f"/api/datasets/{_dataset_ref(dataset)}/review-requests", json=body).json()
|
|
1234
|
+
)
|
|
1235
|
+
typer.echo(f"requested review from {reviewer} on {len(requested['requested'])} case(s)")
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
@review_app.command("show")
|
|
1239
|
+
def review_show(
|
|
1240
|
+
dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
|
|
1241
|
+
case_id: str = typer.Argument(None, help="Case id; omit to list every case's state."),
|
|
1242
|
+
):
|
|
1243
|
+
"""Show review state for a dataset, or state, changelog and comments for one case.
|
|
1244
|
+
|
|
1245
|
+
Example: evalkit review show finance us10k-conv-04
|
|
1246
|
+
"""
|
|
1247
|
+
client = _client()
|
|
1248
|
+
rows = client.get(f"/api/datasets/{_dataset_ref(dataset)}/cases").json()
|
|
1249
|
+
if case_id is None:
|
|
1250
|
+
for ex in rows:
|
|
1251
|
+
review = ex.get("review") or {}
|
|
1252
|
+
typer.echo(
|
|
1253
|
+
f"{ex['case_id']:<40} {review.get('state', '-'):<18} "
|
|
1254
|
+
f"{review.get('requested_to') or ''}"
|
|
1255
|
+
)
|
|
1256
|
+
return
|
|
1257
|
+
match = next((ex for ex in rows if ex["case_id"] == case_id), None)
|
|
1258
|
+
if match is None:
|
|
1259
|
+
raise ApiError(f"unknown case: {case_id}")
|
|
1260
|
+
review = match.get("review") or {}
|
|
1261
|
+
typer.echo(f"state: {review.get('state', '-')}")
|
|
1262
|
+
if review.get("requested_to"):
|
|
1263
|
+
typer.echo(f"requested to: {review['requested_to']}")
|
|
1264
|
+
typer.echo("changelog:")
|
|
1265
|
+
for event in client.get(f"{_case_path(dataset, case_id)}/events").json()["events"]:
|
|
1266
|
+
target = f" → {event['target']}" if event.get("target") else ""
|
|
1267
|
+
typer.echo(
|
|
1268
|
+
f" {event['created_at']} {event['kind']:<18} {event['actor']}{target} "
|
|
1269
|
+
f"[{event['content_hash']}]"
|
|
1270
|
+
)
|
|
1271
|
+
typer.echo("comments:")
|
|
1272
|
+
for thread in client.get(f"{_case_path(dataset, case_id)}/comments").json()["threads"]:
|
|
1273
|
+
typer.echo(_fmt_comment(thread, indent=2))
|
|
1274
|
+
for reply in thread["replies"]:
|
|
1275
|
+
typer.echo(_fmt_comment(reply, indent=6))
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
@review_app.command("approve")
|
|
1279
|
+
def review_approve(
|
|
1280
|
+
dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
|
|
1281
|
+
case_id: str = typer.Argument(..., help="Case id."),
|
|
1282
|
+
comment: str = typer.Option(None, "--comment", help="Optional note."),
|
|
1283
|
+
):
|
|
1284
|
+
"""Approve a case (reviewers only).
|
|
1285
|
+
|
|
1286
|
+
Example: evalkit review approve finance us10k-conv-04
|
|
1287
|
+
"""
|
|
1288
|
+
_post_decision(dataset, case_id, "approve", comment)
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
@review_app.command("request-changes")
|
|
1292
|
+
def review_request_changes(
|
|
1293
|
+
dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
|
|
1294
|
+
case_id: str = typer.Argument(..., help="Case id."),
|
|
1295
|
+
comment: str = typer.Option(..., "--comment", help="What needs to change."),
|
|
1296
|
+
):
|
|
1297
|
+
"""Send a case back to its author with a comment (reviewers only).
|
|
1298
|
+
|
|
1299
|
+
Example: evalkit review request-changes finance us10k-conv-04 --comment "anchor the date"
|
|
1300
|
+
"""
|
|
1301
|
+
_post_decision(dataset, case_id, "request_changes", comment)
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
@review_app.command("comment")
|
|
1305
|
+
def review_comment(
|
|
1306
|
+
dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
|
|
1307
|
+
case_id: str = typer.Argument(..., help="Case id."),
|
|
1308
|
+
text: str = typer.Argument(..., help="Comment body."),
|
|
1309
|
+
reply_to: int = typer.Option(None, "--reply-to", help="Comment id to reply to."),
|
|
1310
|
+
):
|
|
1311
|
+
"""Comment on a case, or reply to a comment.
|
|
1312
|
+
|
|
1313
|
+
Example: evalkit review comment finance us10k-conv-04 "which fiscal year?"
|
|
1314
|
+
"""
|
|
1315
|
+
body = {"body": text, "parent_id": reply_to}
|
|
1316
|
+
stored = _client().post(f"{_case_path(dataset, case_id)}/comments", json=body).json()
|
|
1317
|
+
typer.echo(f"comment {stored['id']} on {case_id}")
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
@review_app.command("resolve")
|
|
1321
|
+
def review_resolve(
|
|
1322
|
+
dataset: str = typer.Argument(..., help="finance (yours) or finance@dev."),
|
|
1323
|
+
case_id: str = typer.Argument(..., help="Case id."),
|
|
1324
|
+
comment_id: int = typer.Argument(..., help="Comment id."),
|
|
1325
|
+
undo: bool = typer.Option(False, "--undo", help="Mark the comment unresolved again."),
|
|
1326
|
+
):
|
|
1327
|
+
"""Resolve (or with --undo, unresolve) a comment.
|
|
1328
|
+
|
|
1329
|
+
Example: evalkit review resolve finance us10k-conv-04 12
|
|
1330
|
+
"""
|
|
1331
|
+
result = (
|
|
1332
|
+
_client()
|
|
1333
|
+
.patch(f"{_case_path(dataset, case_id)}/comments/{comment_id}", json={"resolved": not undo})
|
|
1334
|
+
.json()
|
|
1335
|
+
)
|
|
1336
|
+
typer.echo(f"comment {result['id']} {'resolved' if result['resolved'] else 'unresolved'}")
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
@review_app.command("queue")
|
|
1340
|
+
def review_queue(
|
|
1341
|
+
reviewer: str = typer.Option(None, "--reviewer", help="Whose queue; defaults to you."),
|
|
1342
|
+
everyone: bool = typer.Option(False, "--all", help="Every case under review."),
|
|
1343
|
+
changes_requested: bool = typer.Option(
|
|
1344
|
+
False, "--changes-requested", help="Cases sent back to their authors instead."
|
|
1345
|
+
),
|
|
1346
|
+
):
|
|
1347
|
+
"""List cases waiting for review, or with --changes-requested, cases awaiting an edit.
|
|
1348
|
+
|
|
1349
|
+
Example: evalkit review queue
|
|
1350
|
+
"""
|
|
1351
|
+
client = _client()
|
|
1352
|
+
params: dict = {}
|
|
1353
|
+
if changes_requested:
|
|
1354
|
+
params["state"] = "changes_requested"
|
|
1355
|
+
elif not everyone:
|
|
1356
|
+
params["reviewer"] = reviewer or client.get("/api/auth/me").json()["username"]
|
|
1357
|
+
items = client.get("/api/review/queue", params=params).json()["items"]
|
|
1358
|
+
if not items:
|
|
1359
|
+
typer.echo("nothing waiting")
|
|
1360
|
+
return
|
|
1361
|
+
for item in items:
|
|
1362
|
+
typer.echo(
|
|
1363
|
+
f"{item['dataset']:<20} {item['case_id']:<40} by {item['requested_by']:<16} "
|
|
1364
|
+
f"to {item['requested_to'] or '-':<16} {item['input'][:60]}"
|
|
1365
|
+
)
|