evalkit-py 0.2.0.dev12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalkit/__init__.py +0 -0
- evalkit/api/__init__.py +0 -0
- evalkit/api/access.py +74 -0
- evalkit/api/agents.py +159 -0
- evalkit/api/app.py +360 -0
- evalkit/api/auth.py +225 -0
- evalkit/api/cli_dist.py +10 -0
- evalkit/api/datasets.py +169 -0
- evalkit/api/deps.py +79 -0
- evalkit/api/download.py +15 -0
- evalkit/api/evaluators.py +121 -0
- evalkit/api/experiments.py +616 -0
- evalkit/api/fixtures.py +95 -0
- evalkit/api/harness_check.py +48 -0
- evalkit/api/imports.py +180 -0
- evalkit/api/limits.py +84 -0
- evalkit/api/llm_providers.py +120 -0
- evalkit/api/mcp/__init__.py +0 -0
- evalkit/api/mcp/auth.py +100 -0
- evalkit/api/mcp/descriptions.py +336 -0
- evalkit/api/mcp/models.py +259 -0
- evalkit/api/mcp/server.py +101 -0
- evalkit/api/mcp/tools.py +844 -0
- evalkit/api/meta.py +19 -0
- evalkit/api/otel.py +350 -0
- evalkit/api/pull.py +197 -0
- evalkit/api/review.py +271 -0
- evalkit/api/runs.py +555 -0
- evalkit/api/server_keys.py +44 -0
- evalkit/api/tokens.py +51 -0
- evalkit/api/trace_imports.py +173 -0
- evalkit/api/trace_sources.py +49 -0
- evalkit/api/traces.py +196 -0
- evalkit/api/users.py +72 -0
- evalkit/api/workspaces.py +131 -0
- evalkit/bundled/contracts/atif-accepted.json +5 -0
- evalkit/bundled/contracts/custom-evaluator-api-v1.json +243 -0
- evalkit/bundled/contracts/harness-api-v1.json +938 -0
- evalkit/bundled/contracts/span-mapping-v1.json +314 -0
- evalkit/bundled/docs/concepts.md +26 -0
- evalkit/bundled/docs/config.md +38 -0
- evalkit/bundled/docs/harness-api.md +44 -0
- evalkit/bundled/docs/install.md +60 -0
- evalkit/bundled/docs/login.md +36 -0
- evalkit/bundled/docs/mappings.md +136 -0
- evalkit/bundled/docs/mcp.md +44 -0
- evalkit/bundled/docs/quickstart.md +142 -0
- evalkit/bundled/docs/sdk-runs.md +171 -0
- evalkit/bundled/docs/tracing.md +171 -0
- evalkit/bundled/docs/try-locally.md +288 -0
- evalkit/bundled/docs/workspaces.md +45 -0
- evalkit/bundled/skills/authoring-eval-cases/SKILL.md +387 -0
- evalkit/bundled/skills/authoring-eval-cases/references/overlays.md +83 -0
- evalkit/bundled/skills/authoring-eval-cases/references/tool-checks.md +71 -0
- evalkit/bundled/skills/connect-traces/SKILL.md +296 -0
- evalkit/bundled/skills/connect-traces/references/gen_ai.json +42 -0
- evalkit/bundled/skills/connect-traces/references/openinference.json +35 -0
- evalkit/bundled/skills/connect-traces/references/span-mapping-schema.md +88 -0
- evalkit/bundled/skills/eval-driven-development/SKILL.md +458 -0
- evalkit/bundled/skills/eval-driven-development/references/analyze-run.md +97 -0
- evalkit/bundled/skills/eval-driven-development/references/diagnose.md +130 -0
- evalkit/bundled/skills/eval-driven-development/references/synthesize-cases.md +87 -0
- evalkit/bundled/skills/eval-driven-development/references/synthesize.md +165 -0
- evalkit/bundled/skills/evalkit-deployment/SKILL.md +332 -0
- evalkit/bundled/skills/evalkit-deployment/config.example.yaml +49 -0
- evalkit/bundled/skills/evalkit-deployment/docker-compose.yml +51 -0
- evalkit/bundled/skills/evalkit-deployment/references/config-and-deploy.md +333 -0
- evalkit/bundled/skills/evalkit-deployment/references/harness-api.md +139 -0
- evalkit/bundled/skills/evalkit-deployment/try/.env.example +11 -0
- evalkit/bundled/skills/evalkit-deployment/try/agent.py +131 -0
- evalkit/bundled/skills/evalkit-deployment/try/cases/acme/balance-sheet/cases.yaml +25 -0
- evalkit/bundled/skills/evalkit-deployment/try/cases/acme/income-statement/cases.yaml +43 -0
- evalkit/bundled/skills/evalkit-deployment/try/config.yaml +17 -0
- evalkit/bundled/skills/evalkit-deployment/try/docker-compose.yml +82 -0
- evalkit/bundled/skills/evalkit-deployment/try/evalkit.yaml +4 -0
- evalkit/bundled/skills/evalkit-deployment/try/mapping.json +42 -0
- evalkit/bundled/skills/evalkit-deployment/try/run.py +33 -0
- evalkit/bundled/skills/evalkit-usage/SKILL.md +509 -0
- evalkit/bundled/skills/evalkit-usage/references/suite-fixtures.md +42 -0
- evalkit/bundled/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- evalkit/bundled/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- evalkit/bundled/ui/assets/abap-08VXUWAP.js +1 -0
- evalkit/bundled/ui/assets/apex-BWPQTe0t.js +1 -0
- evalkit/bundled/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- evalkit/bundled/ui/assets/bat-i0X4ZdIN.js +1 -0
- evalkit/bundled/ui/assets/bicep-B5-_aFwp.js +2 -0
- evalkit/bundled/ui/assets/cameligo-DMUM7wLl.js +1 -0
- evalkit/bundled/ui/assets/clojure-Cm7r79vr.js +1 -0
- evalkit/bundled/ui/assets/codicon-DF1abBS2.ttf +0 -0
- evalkit/bundled/ui/assets/coffee-Ba7i2nA0.js +1 -0
- evalkit/bundled/ui/assets/cpp-C7h46wYY.js +1 -0
- evalkit/bundled/ui/assets/csharp-BKxtCVv1.js +1 -0
- evalkit/bundled/ui/assets/csp-bTuwJoIa.js +1 -0
- evalkit/bundled/ui/assets/css-DIMkf-bt.js +3 -0
- evalkit/bundled/ui/assets/css.worker-DwfUZ3fB.js +96 -0
- evalkit/bundled/ui/assets/cssMode-C97l1pbB.js +1 -0
- evalkit/bundled/ui/assets/cypher-CVaqCwHa.js +1 -0
- evalkit/bundled/ui/assets/dart-onAF5SnQ.js +1 -0
- evalkit/bundled/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- evalkit/bundled/ui/assets/ecl-94qehhXe.js +1 -0
- evalkit/bundled/ui/assets/editor-DjpsE8to.css +1 -0
- evalkit/bundled/ui/assets/editor.api-NTesIxw5.js +908 -0
- evalkit/bundled/ui/assets/editor.worker-CRz__AKW.js +33 -0
- evalkit/bundled/ui/assets/elixir-6RTg0lbw.js +1 -0
- evalkit/bundled/ui/assets/flow9-C5_-GSwl.js +1 -0
- evalkit/bundled/ui/assets/freemarker2-BWqEFagp.js +3 -0
- evalkit/bundled/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- evalkit/bundled/ui/assets/go-C-y9NEjX.js +1 -0
- evalkit/bundled/ui/assets/graphql-fmXr3nnJ.js +1 -0
- evalkit/bundled/ui/assets/handlebars-DkIF-whN.js +1 -0
- evalkit/bundled/ui/assets/hcl-CpzslTdj.js +1 -0
- evalkit/bundled/ui/assets/html-C_rpeQXJ.js +1 -0
- evalkit/bundled/ui/assets/html.worker-D9ugpX4y.js +509 -0
- evalkit/bundled/ui/assets/htmlMode-B0YIq3DF.js +1 -0
- evalkit/bundled/ui/assets/index-Cw-jBH6M.js +736 -0
- evalkit/bundled/ui/assets/index-DKtoQZtm.css +1 -0
- evalkit/bundled/ui/assets/ini-sBoK_t0W.js +1 -0
- evalkit/bundled/ui/assets/java-BEtHBSE6.js +1 -0
- evalkit/bundled/ui/assets/javascript-D4EGhYA6.js +1 -0
- evalkit/bundled/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- evalkit/bundled/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- evalkit/bundled/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- evalkit/bundled/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- evalkit/bundled/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- evalkit/bundled/ui/assets/json.worker-CV9fqkUp.js +65 -0
- evalkit/bundled/ui/assets/jsonMode-pQjSfIH6.js +7 -0
- evalkit/bundled/ui/assets/julia-Bri6UV-V.js +1 -0
- evalkit/bundled/ui/assets/kotlin-BOotOW0E.js +1 -0
- evalkit/bundled/ui/assets/less-B9JPFI3C.js +2 -0
- evalkit/bundled/ui/assets/lexon-CfSJPG6W.js +1 -0
- evalkit/bundled/ui/assets/liquid-DbrvoYjD.js +1 -0
- evalkit/bundled/ui/assets/lspLanguageFeatures-D0B7cH6y.js +4 -0
- evalkit/bundled/ui/assets/lua-CsQS60Ue.js +1 -0
- evalkit/bundled/ui/assets/m3-D-oSqn_W.js +1 -0
- evalkit/bundled/ui/assets/markdown-Cimd5fb3.js +1 -0
- evalkit/bundled/ui/assets/mdx-B0HrujLR.js +1 -0
- evalkit/bundled/ui/assets/mips-CIPQ_RoX.js +1 -0
- evalkit/bundled/ui/assets/monacoEditor-DsoPncxW.js +68 -0
- evalkit/bundled/ui/assets/monacoEditor-n7clI0si.css +1 -0
- evalkit/bundled/ui/assets/msdax-DauUninz.js +1 -0
- evalkit/bundled/ui/assets/mysql-SOo6toE5.js +1 -0
- evalkit/bundled/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- evalkit/bundled/ui/assets/pascal-DrH0SRf2.js +1 -0
- evalkit/bundled/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- evalkit/bundled/ui/assets/perl-oz_6vUea.js +1 -0
- evalkit/bundled/ui/assets/pgsql-DTj74zXo.js +1 -0
- evalkit/bundled/ui/assets/php-nr791fC2.js +1 -0
- evalkit/bundled/ui/assets/pla-CopQ2nXW.js +1 -0
- evalkit/bundled/ui/assets/postiats-43DmfD33.js +1 -0
- evalkit/bundled/ui/assets/powerquery-D3hlyOfw.js +1 -0
- evalkit/bundled/ui/assets/powershell-DmHpPYUd.js +1 -0
- evalkit/bundled/ui/assets/protobuf-C531GsRP.js +2 -0
- evalkit/bundled/ui/assets/pug-Z5eAx3Zn.js +1 -0
- evalkit/bundled/ui/assets/python-CieHVM9n.js +1 -0
- evalkit/bundled/ui/assets/qsharp-DkqhCAOL.js +1 -0
- evalkit/bundled/ui/assets/r-BwWrilGY.js +1 -0
- evalkit/bundled/ui/assets/razor-sLewSMyp.js +1 -0
- evalkit/bundled/ui/assets/redis-ClamHrr6.js +1 -0
- evalkit/bundled/ui/assets/redshift-DT7zqm-g.js +1 -0
- evalkit/bundled/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- evalkit/bundled/ui/assets/ruby-DezsRK8O.js +1 -0
- evalkit/bundled/ui/assets/rust-DdL9SqIa.js +1 -0
- evalkit/bundled/ui/assets/sb-CcwsVR0C.js +1 -0
- evalkit/bundled/ui/assets/scala-DHpiXF5c.js +1 -0
- evalkit/bundled/ui/assets/scheme-BeGwcela.js +1 -0
- evalkit/bundled/ui/assets/scss-gp-XZpBa.js +3 -0
- evalkit/bundled/ui/assets/shell-CC2rA5mh.js +1 -0
- evalkit/bundled/ui/assets/solidity-BEEn4gHE.js +1 -0
- evalkit/bundled/ui/assets/sophia-CRfGWb83.js +1 -0
- evalkit/bundled/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- evalkit/bundled/ui/assets/sql-NEE52Syq.js +1 -0
- evalkit/bundled/ui/assets/st-DbInun42.js +1 -0
- evalkit/bundled/ui/assets/swift-Bxkupp3x.js +1 -0
- evalkit/bundled/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- evalkit/bundled/ui/assets/tcl-DISqw1ZD.js +1 -0
- evalkit/bundled/ui/assets/ts.worker-DIAQkoB3.js +67741 -0
- evalkit/bundled/ui/assets/tsMode-BcVQ1nu0.js +11 -0
- evalkit/bundled/ui/assets/twig-De2hgUGE.js +1 -0
- evalkit/bundled/ui/assets/typescript-CUvNWd9p.js +1 -0
- evalkit/bundled/ui/assets/typespec-B8J7ngcE.js +1 -0
- evalkit/bundled/ui/assets/vb-DV3o63ZY.js +1 -0
- evalkit/bundled/ui/assets/wgsl-DpFanUEy.js +298 -0
- evalkit/bundled/ui/assets/workers-CwpYQtzp.js +1 -0
- evalkit/bundled/ui/assets/xml-C1HnXMdF.js +1 -0
- evalkit/bundled/ui/assets/yaml-D7Ey3HQu.js +1 -0
- evalkit/bundled/ui/favicon.svg +6 -0
- evalkit/bundled/ui/index.html +14 -0
- evalkit/cli/__init__.py +0 -0
- evalkit/cli/admin_commands.py +590 -0
- evalkit/cli/client.py +67 -0
- evalkit/cli/init_server.py +165 -0
- evalkit/cli/local_run.py +195 -0
- evalkit/cli/main.py +1365 -0
- evalkit/cli/runs_commands.py +539 -0
- evalkit/cli/server_commands.py +130 -0
- evalkit/cli/skill_targets.py +43 -0
- evalkit/cli/tokens_commands.py +52 -0
- evalkit/cli/traces_commands.py +332 -0
- evalkit/cli/update.py +85 -0
- evalkit/client/__init__.py +0 -0
- evalkit/client/agents.py +25 -0
- evalkit/client/api.py +193 -0
- evalkit/client/blob.py +104 -0
- evalkit/client/connection.py +88 -0
- evalkit/client/evaluators.py +11 -0
- evalkit/client/sessions.py +17 -0
- evalkit/client/store.py +139 -0
- evalkit/client/summary.py +46 -0
- evalkit/client/sync.py +198 -0
- evalkit/config.py +227 -0
- evalkit/contracts/__init__.py +0 -0
- evalkit/contracts/atif.py +115 -0
- evalkit/contracts/cases.py +456 -0
- evalkit/contracts/custom_evaluator.py +58 -0
- evalkit/contracts/enums.py +86 -0
- evalkit/contracts/esf.py +120 -0
- evalkit/contracts/harness.py +281 -0
- evalkit/contracts/notes.json +4 -0
- evalkit/contracts/notes.py +7 -0
- evalkit/contracts/settings.py +41 -0
- evalkit/core/__init__.py +0 -0
- evalkit/core/access.py +75 -0
- evalkit/core/agents.py +65 -0
- evalkit/core/blob.py +46 -0
- evalkit/core/capabilities.py +36 -0
- evalkit/core/costmap.py +90 -0
- evalkit/core/custom_eval.py +50 -0
- evalkit/core/data/cost_map.json +81546 -0
- evalkit/core/endpoints.py +31 -0
- evalkit/core/evaluators/__init__.py +29 -0
- evalkit/core/evaluators/base.py +121 -0
- evalkit/core/evaluators/gauges.py +84 -0
- evalkit/core/evaluators/presentation.py +166 -0
- evalkit/core/evaluators/response_time.py +21 -0
- evalkit/core/evaluators/rubrics.py +494 -0
- evalkit/core/evaluators/tool_trajectory.py +187 -0
- evalkit/core/evaluators/trajectory_rubrics.py +198 -0
- evalkit/core/fixtures.py +57 -0
- evalkit/core/harness.py +227 -0
- evalkit/core/harness_check.py +191 -0
- evalkit/core/importer.py +257 -0
- evalkit/core/judge.py +78 -0
- evalkit/core/lint.py +265 -0
- evalkit/core/overlays.py +76 -0
- evalkit/core/paths.py +25 -0
- evalkit/core/promotion.py +94 -0
- evalkit/core/pull.py +198 -0
- evalkit/core/python_eval.py +59 -0
- evalkit/core/remote_evaluators.py +32 -0
- evalkit/core/review.py +220 -0
- evalkit/core/run_digest.py +421 -0
- evalkit/core/runner.py +995 -0
- evalkit/core/scorecard.py +255 -0
- evalkit/core/traces/__init__.py +0 -0
- evalkit/core/traces/convert.py +336 -0
- evalkit/core/traces/importer.py +113 -0
- evalkit/core/traces/mapping.py +385 -0
- evalkit/core/traces/poll.py +38 -0
- evalkit/core/traces/raw.py +21 -0
- evalkit/core/traces/sessions.py +172 -0
- evalkit/core/traces/settle.py +14 -0
- evalkit/core/traces/sources/__init__.py +19 -0
- evalkit/core/traces/sources/base.py +25 -0
- evalkit/core/traces/sources/phoenix.py +121 -0
- evalkit/core/trajectory.py +248 -0
- evalkit/core/workspace.py +30 -0
- evalkit/credentials.py +119 -0
- evalkit/db/__init__.py +0 -0
- evalkit/db/agents.py +289 -0
- evalkit/db/alembic/env.py +90 -0
- evalkit/db/alembic/script.py.mako +28 -0
- evalkit/db/alembic/versions/0011_baseline.py +1002 -0
- evalkit/db/alembic/versions/0012_ticket_token_kind.py +27 -0
- evalkit/db/alembic/versions/0013_keys_and_llm_providers.py +67 -0
- evalkit/db/cases.py +608 -0
- evalkit/db/crypto.py +12 -0
- evalkit/db/evaluators.py +156 -0
- evalkit/db/experiments.py +363 -0
- evalkit/db/fixture_blobs.py +91 -0
- evalkit/db/keys.py +78 -0
- evalkit/db/llm_providers.py +176 -0
- evalkit/db/locks.py +6 -0
- evalkit/db/mappings.py +168 -0
- evalkit/db/models.py +567 -0
- evalkit/db/promotion.py +271 -0
- evalkit/db/rekey.py +46 -0
- evalkit/db/review.py +250 -0
- evalkit/db/review_state.py +50 -0
- evalkit/db/runs.py +533 -0
- evalkit/db/scorecard.py +300 -0
- evalkit/db/session.py +71 -0
- evalkit/db/sessions.py +90 -0
- evalkit/db/spans.py +360 -0
- evalkit/db/store.py +209 -0
- evalkit/db/tokens.py +100 -0
- evalkit/db/trace_imports.py +308 -0
- evalkit/db/trace_sources.py +131 -0
- evalkit/db/users.py +186 -0
- evalkit/db/workspaces.py +232 -0
- evalkit/paths.py +33 -0
- evalkit/sdk/__init__.py +29 -0
- evalkit/sdk/binding.py +69 -0
- evalkit/sdk/evaluators.py +56 -0
- evalkit/sdk/runner.py +957 -0
- evalkit/sdk/tracing.py +166 -0
- evalkit/store/__init__.py +0 -0
- evalkit/store/base.py +354 -0
- evalkit/store/blob.py +69 -0
- evalkit/version.py +44 -0
- evalkit_py-0.2.0.dev12.dist-info/METADATA +205 -0
- evalkit_py-0.2.0.dev12.dist-info/RECORD +371 -0
- evalkit_py-0.2.0.dev12.dist-info/WHEEL +4 -0
- evalkit_py-0.2.0.dev12.dist-info/entry_points.txt +2 -0
- evalkit_py-0.2.0.dev12.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import zipfile
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from datetime import UTC, datetime, timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from urllib.parse import quote
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from evalkit.cli.client import ApiClient, ApiError
|
|
19
|
+
from evalkit.client.api import encode
|
|
20
|
+
from evalkit.contracts.atif import TrajectoryValidationError
|
|
21
|
+
from evalkit.contracts.harness import RunListItem
|
|
22
|
+
from evalkit.core.agents import AgentDefinition, Driver, driver
|
|
23
|
+
from evalkit.core.harness import HarnessClient
|
|
24
|
+
from evalkit.core.paths import UnsafePathError, is_safe_run_id
|
|
25
|
+
from evalkit.core.pull import (
|
|
26
|
+
MAX_PULL,
|
|
27
|
+
SCAN_NOTE,
|
|
28
|
+
RunBundle,
|
|
29
|
+
bundle_files,
|
|
30
|
+
index_row,
|
|
31
|
+
list_harness_runs,
|
|
32
|
+
merge_index,
|
|
33
|
+
safe_dir,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
runs_app = typer.Typer(no_args_is_help=True, help="List and pull an agent's runs (app or eval).")
|
|
37
|
+
|
|
38
|
+
RELATIVE = re.compile(r"^(\d+)([dhm])$")
|
|
39
|
+
ISO = re.compile(r"^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?)?(Z|[+-]\d{2}:?\d{2})?$")
|
|
40
|
+
UNITS = {"d": "days", "h": "hours", "m": "minutes"}
|
|
41
|
+
SERVER_PAGE = 200
|
|
42
|
+
DEFAULT_ROOT = Path("evalkit-runs")
|
|
43
|
+
PULL_HELP = (
|
|
44
|
+
"Download runs with their ATIF trajectories into a local directory: index.json plus "
|
|
45
|
+
"runs/<run_id>/ (run.json, trajectory.json, and for eval runs case.json and "
|
|
46
|
+
"evaluations.json).\n\n"
|
|
47
|
+
"Workspace files and full tool outputs are not fetched: the trajectory keeps the "
|
|
48
|
+
"shortened tool output the agent saw. List and fetch a run's files with "
|
|
49
|
+
"evalkit runs files.\n\n"
|
|
50
|
+
"Writes to ./evalkit-runs/… by default. Add that directory to .gitignore, or pass "
|
|
51
|
+
"--out with a path outside the repo. Pulling again into the same --out adds to it.\n\n"
|
|
52
|
+
"Example: evalkit runs pull ouragent@prod --source app --since 7d --limit 25 --offset 0"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
FILES_HELP = (
|
|
56
|
+
"List a pulled run's workspace files, or fetch some of them. Without --path it prints "
|
|
57
|
+
"the file tree (size and path) and writes it to runs/<run_id>/files.json. Each --path, "
|
|
58
|
+
"a file or a folder for everything under it, is fetched into "
|
|
59
|
+
"runs/<run_id>/files/<path>, one request per file, each written as it arrives; the "
|
|
60
|
+
"first failure stops the command and names the file. A path not in the tree, such as "
|
|
61
|
+
"a tool result's extra.full_output_path from trajectory.json, is fetched directly.\n\n"
|
|
62
|
+
"Example: evalkit runs files evalkit-runs/ouragent@prod/20261023-101500 RUN_ID "
|
|
63
|
+
"--path outputs/report.md"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
LocalHarness = Callable[[AgentDefinition], HarnessClient]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_when(text: str | None, now: datetime | None = None) -> datetime | None:
|
|
70
|
+
if text is None:
|
|
71
|
+
return None
|
|
72
|
+
match = RELATIVE.match(text)
|
|
73
|
+
if match:
|
|
74
|
+
return (now or datetime.now(UTC)) - timedelta(**{UNITS[match[2]]: int(match[1])})
|
|
75
|
+
if not ISO.match(text):
|
|
76
|
+
raise ApiError(f"not a time: {text!r}; use ISO 8601 (2026-10-08T00:00Z) or 7d / 12h / 30m")
|
|
77
|
+
parsed = datetime.fromisoformat(text)
|
|
78
|
+
return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _iso(value: datetime | None) -> str | None:
|
|
82
|
+
return value.isoformat() if value else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _list(client: ApiClient, agent: str, params: dict) -> dict:
|
|
86
|
+
query = {k: v for k, v in params.items() if v is not None}
|
|
87
|
+
return client.get(f"/api/agents/{encode(agent)}/runs", params=query).json()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _list_stored(client: ApiClient, agent: str, filters: dict, wanted: int) -> tuple[list, int]:
|
|
91
|
+
rows: list[dict] = []
|
|
92
|
+
total = 0
|
|
93
|
+
while len(rows) < wanted:
|
|
94
|
+
size = min(SERVER_PAGE, wanted - len(rows))
|
|
95
|
+
body = _list(client, agent, {**filters, "limit": size, "offset": len(rows)})
|
|
96
|
+
rows += body["runs"]
|
|
97
|
+
total = body["total"]
|
|
98
|
+
if len(body["runs"]) < size:
|
|
99
|
+
break
|
|
100
|
+
return rows, total
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _known(client: ApiClient, agent: str, run_ids: list[str]) -> set[str]:
|
|
104
|
+
known: set[str] = set()
|
|
105
|
+
for start in range(0, len(run_ids), MAX_PULL):
|
|
106
|
+
chunk = run_ids[start : start + MAX_PULL]
|
|
107
|
+
body = client.get(
|
|
108
|
+
f"/api/agents/{encode(agent)}/runs", params=[("run_id", i) for i in chunk]
|
|
109
|
+
).json()
|
|
110
|
+
known |= {row["run_id"] for row in body["runs"]}
|
|
111
|
+
return known
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _unzip(content: bytes) -> tuple[dict, dict[str, bytes]]:
|
|
115
|
+
archive = zipfile.ZipFile(io.BytesIO(content))
|
|
116
|
+
files = {m.filename: archive.read(m) for m in archive.infolist() if not m.is_dir()}
|
|
117
|
+
return json.loads(files.pop("index.json")), files
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _server_pull_ids(client: ApiClient, agent: str, run_ids: list[str]) -> tuple[list, dict]:
|
|
121
|
+
rows: list[dict] = []
|
|
122
|
+
files: dict[str, bytes] = {}
|
|
123
|
+
for start in range(0, len(run_ids), MAX_PULL):
|
|
124
|
+
chunk = run_ids[start : start + MAX_PULL]
|
|
125
|
+
resp = client.post(f"/api/agents/{encode(agent)}/runs/pull", json={"run_ids": chunk})
|
|
126
|
+
index, chunk_files = _unzip(resp.content)
|
|
127
|
+
rows += index["runs"]
|
|
128
|
+
files |= chunk_files
|
|
129
|
+
return rows, files
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def write_pull(target: Path, index: dict, files: dict[str, bytes]) -> None:
|
|
133
|
+
target = write_files(target, files)
|
|
134
|
+
existing_path = target / "index.json"
|
|
135
|
+
existing = json.loads(existing_path.read_text()) if existing_path.is_file() else None
|
|
136
|
+
existing_path.write_text(json.dumps(merge_index(existing, index), indent=2) + "\n")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def write_files(target: Path, files: dict[str, bytes]) -> Path:
|
|
140
|
+
target = target.resolve()
|
|
141
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
# Member names carry run ids and harness-reported paths — validate each one
|
|
143
|
+
# resolves under the destination before writing.
|
|
144
|
+
for name, data in files.items():
|
|
145
|
+
destination = (target / name).resolve()
|
|
146
|
+
if not destination.is_relative_to(target):
|
|
147
|
+
raise ApiError(f"archive member escapes the output directory: {name!r}")
|
|
148
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
destination.write_bytes(data)
|
|
150
|
+
return target
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _stamp(target: Path, filters: dict, total: int | None = None) -> None:
|
|
154
|
+
path = target.resolve() / "index.json"
|
|
155
|
+
index = json.loads(path.read_text())
|
|
156
|
+
index["filters"] = filters
|
|
157
|
+
if total is not None:
|
|
158
|
+
index["total_matching"] = total
|
|
159
|
+
path.write_text(json.dumps(index, indent=2) + "\n")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def unignored_in_git(target: Path) -> bool:
|
|
163
|
+
if shutil.which("git") is None:
|
|
164
|
+
return False
|
|
165
|
+
probe = target if target.exists() else target.parent
|
|
166
|
+
while not probe.exists():
|
|
167
|
+
probe = probe.parent
|
|
168
|
+
inside = subprocess.run(
|
|
169
|
+
["git", "-C", str(probe), "rev-parse", "--is-inside-work-tree"],
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
)
|
|
173
|
+
if inside.returncode != 0 or inside.stdout.strip() != "true":
|
|
174
|
+
return False
|
|
175
|
+
ignored = subprocess.run(
|
|
176
|
+
["git", "-C", str(probe), "check-ignore", "-q", str(target.resolve())],
|
|
177
|
+
capture_output=True,
|
|
178
|
+
)
|
|
179
|
+
return ignored.returncode != 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def _harness_rows(
|
|
183
|
+
harness: HarnessClient,
|
|
184
|
+
known: Callable[[list[str]], set[str]],
|
|
185
|
+
since: datetime | None,
|
|
186
|
+
until: datetime | None,
|
|
187
|
+
wanted: int,
|
|
188
|
+
) -> tuple[list[RunListItem], int, str | None]:
|
|
189
|
+
async def exclude(run_ids: list[str]) -> set[str]:
|
|
190
|
+
return known(run_ids)
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
listed = await list_harness_runs(
|
|
194
|
+
harness, wanted=wanted, since=since, until=until, exclude=exclude
|
|
195
|
+
)
|
|
196
|
+
finally:
|
|
197
|
+
await harness.aclose()
|
|
198
|
+
return listed.runs, listed.total, SCAN_NOTE if listed.capped else None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _require_run_id(run_id: str) -> None:
|
|
202
|
+
if not is_safe_run_id(run_id):
|
|
203
|
+
raise ApiError(f"run id {run_id!r} must be one plain path segment")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def _harness_bundle(harness: HarnessClient, row: dict) -> RunBundle:
|
|
207
|
+
run_id = row["run_id"]
|
|
208
|
+
bundle = RunBundle(
|
|
209
|
+
run_id=run_id,
|
|
210
|
+
source="app",
|
|
211
|
+
created_at=row.get("created_at"),
|
|
212
|
+
title=row.get("title"),
|
|
213
|
+
run={"run_id": run_id, "source": "app"},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
bundle.trajectory = (await harness.trajectory(run_id)).model_dump(exclude_none=True)
|
|
218
|
+
except (httpx.HTTPError, TrajectoryValidationError, UnsafePathError) as exc:
|
|
219
|
+
bundle.pull_error = str(exc)
|
|
220
|
+
return bundle
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def _harness_bundles(harness: HarnessClient, rows: list[dict]) -> list[RunBundle]:
|
|
224
|
+
bundles = [await _harness_bundle(harness, row) for row in rows]
|
|
225
|
+
await harness.aclose()
|
|
226
|
+
return bundles
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _sort_key(row: dict) -> datetime:
|
|
230
|
+
created = row.get("created_at")
|
|
231
|
+
if not created:
|
|
232
|
+
return datetime.min.replace(tzinfo=UTC)
|
|
233
|
+
parsed = datetime.fromisoformat(created)
|
|
234
|
+
return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _local_rows(
|
|
238
|
+
client: ApiClient,
|
|
239
|
+
definition: AgentDefinition,
|
|
240
|
+
harness: LocalHarness,
|
|
241
|
+
filters: dict,
|
|
242
|
+
limit: int,
|
|
243
|
+
offset: int,
|
|
244
|
+
) -> tuple[list[dict], int, str | None]:
|
|
245
|
+
agent = definition.ref
|
|
246
|
+
stored, stored_total = _list_stored(client, agent, filters, limit + offset)
|
|
247
|
+
extra: list[dict] = []
|
|
248
|
+
extra_total = 0
|
|
249
|
+
note: str | None = None
|
|
250
|
+
if filters["source"] != "eval":
|
|
251
|
+
try:
|
|
252
|
+
listed, extra_total, note = asyncio.run(
|
|
253
|
+
_harness_rows(
|
|
254
|
+
harness(definition),
|
|
255
|
+
lambda run_ids: _known(client, agent, run_ids),
|
|
256
|
+
parse_when(filters["since"]),
|
|
257
|
+
parse_when(filters["until"]),
|
|
258
|
+
limit + offset,
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
except httpx.HTTPError as exc:
|
|
262
|
+
listed, note = [], f"harness unreachable: {exc}"
|
|
263
|
+
extra = [
|
|
264
|
+
{**run.model_dump(mode="json"), "source": "app", "stored": False} for run in listed
|
|
265
|
+
]
|
|
266
|
+
rows = sorted([*stored, *extra], key=_sort_key, reverse=True)
|
|
267
|
+
return rows[offset : offset + limit], stored_total + extra_total, note
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _pull_local(
|
|
271
|
+
client: ApiClient,
|
|
272
|
+
definition: AgentDefinition,
|
|
273
|
+
harness: LocalHarness,
|
|
274
|
+
rows: list[dict],
|
|
275
|
+
) -> tuple[dict, dict[str, bytes]]:
|
|
276
|
+
agent = definition.ref
|
|
277
|
+
known = _known(client, agent, [row["run_id"] for row in rows])
|
|
278
|
+
index_rows, files = _server_pull_ids(
|
|
279
|
+
client, agent, [row["run_id"] for row in rows if row["run_id"] in known]
|
|
280
|
+
)
|
|
281
|
+
local = asyncio.run(
|
|
282
|
+
_harness_bundles(harness(definition), [r for r in rows if r["run_id"] not in known])
|
|
283
|
+
)
|
|
284
|
+
for bundle in local:
|
|
285
|
+
files |= bundle_files(bundle)
|
|
286
|
+
return {
|
|
287
|
+
"agent": agent,
|
|
288
|
+
"pulled_at": datetime.now(UTC).isoformat(),
|
|
289
|
+
"runs": index_rows + [index_row(bundle) for bundle in local],
|
|
290
|
+
}, files
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def register(
|
|
294
|
+
app: typer.Typer,
|
|
295
|
+
client_factory: Callable[[], ApiClient],
|
|
296
|
+
agent_definition: Callable[[str], AgentDefinition],
|
|
297
|
+
local_harness: LocalHarness,
|
|
298
|
+
) -> None:
|
|
299
|
+
app.add_typer(runs_app, name="runs")
|
|
300
|
+
|
|
301
|
+
@runs_app.command("list")
|
|
302
|
+
def runs_list(
|
|
303
|
+
agent: str = typer.Argument(..., help="myagent (yours) or ouragent@dev."),
|
|
304
|
+
source: str = typer.Option("all", "--source", help="app, eval or all."),
|
|
305
|
+
since: str | None = typer.Option(None, "--since", help="ISO time, or 7d / 12h / 30m."),
|
|
306
|
+
until: str | None = typer.Option(None, "--until", help="ISO time, or 7d / 12h / 30m."),
|
|
307
|
+
limit: int = typer.Option(50, "--limit"),
|
|
308
|
+
offset: int = typer.Option(0, "--offset"),
|
|
309
|
+
as_json: bool = typer.Option(False, "--json", help="One JSON object per line."),
|
|
310
|
+
):
|
|
311
|
+
"""List an agent's runs: app runs (outside any experiment) and eval runs.
|
|
312
|
+
|
|
313
|
+
Example: evalkit runs list ouragent@prod --source app --since 7d
|
|
314
|
+
"""
|
|
315
|
+
client = client_factory()
|
|
316
|
+
definition = agent_definition(agent)
|
|
317
|
+
filters = _filters(source, parse_when(since), parse_when(until))
|
|
318
|
+
note = None
|
|
319
|
+
if driver(definition) is Driver.CLI:
|
|
320
|
+
rows, total, note = _local_rows(
|
|
321
|
+
client, definition, local_harness, filters, limit, offset
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
body = _list(client, definition.ref, {**filters, "limit": limit, "offset": offset})
|
|
325
|
+
rows, total, note = body["runs"], body["total"], body.get("harness_note")
|
|
326
|
+
if driver(definition) is Driver.SDK and not definition.is_otel and source != "eval":
|
|
327
|
+
note = (
|
|
328
|
+
f"{definition.ref} has no harness and no traces, so evalkit only sees its eval runs"
|
|
329
|
+
)
|
|
330
|
+
if as_json:
|
|
331
|
+
for row in rows:
|
|
332
|
+
typer.echo(json.dumps(row))
|
|
333
|
+
typer.echo(f"{len(rows)} of {total} run(s)", err=True)
|
|
334
|
+
if note:
|
|
335
|
+
typer.echo(f"note: {note}", err=True)
|
|
336
|
+
return
|
|
337
|
+
for row in rows:
|
|
338
|
+
where = (
|
|
339
|
+
f"exp {row['experiment_id']} {row['case_id']}" if row.get("experiment_id") else ""
|
|
340
|
+
)
|
|
341
|
+
typer.echo(
|
|
342
|
+
f"{row['run_id']:<38} {row['source']:<4} {row.get('status') or '-':<10} "
|
|
343
|
+
f"{row.get('created_at') or '-':<26} {where} {row.get('title') or ''}".rstrip()
|
|
344
|
+
)
|
|
345
|
+
typer.echo(f"{len(rows)} of {total} run(s)")
|
|
346
|
+
if note:
|
|
347
|
+
typer.echo(f"note: {note}")
|
|
348
|
+
|
|
349
|
+
@runs_app.command("pull", help=PULL_HELP)
|
|
350
|
+
def runs_pull(
|
|
351
|
+
agent: str | None = typer.Argument(None, help="myagent (yours) or ouragent@dev."),
|
|
352
|
+
source: str = typer.Option("all", "--source", help="app, eval or all."),
|
|
353
|
+
since: str | None = typer.Option(None, "--since", help="ISO time, or 7d / 12h / 30m."),
|
|
354
|
+
until: str | None = typer.Option(None, "--until", help="ISO time, or 7d / 12h / 30m."),
|
|
355
|
+
limit: int | None = typer.Option(
|
|
356
|
+
None,
|
|
357
|
+
"--limit",
|
|
358
|
+
help=f"Runs to pull; default {MAX_PULL}. Above {MAX_PULL} on purpose only.",
|
|
359
|
+
),
|
|
360
|
+
offset: int = typer.Option(0, "--offset", help="Skip this many matching runs."),
|
|
361
|
+
run_id: list[str] = typer.Option([], "--run-id", help="Pull these runs only."),
|
|
362
|
+
experiment: int | None = typer.Option(None, "--experiment", help="Pull every run of it."),
|
|
363
|
+
out: Path | None = typer.Option(None, "--out", help="Output directory."),
|
|
364
|
+
):
|
|
365
|
+
client = client_factory()
|
|
366
|
+
if experiment is not None:
|
|
367
|
+
ref = client.get(f"/api/experiments/{experiment}").json()["agent"]
|
|
368
|
+
target = out or DEFAULT_ROOT / f"experiment-{experiment}"
|
|
369
|
+
pulled = 0
|
|
370
|
+
while True:
|
|
371
|
+
resp = client.post(
|
|
372
|
+
f"/api/agents/{encode(ref)}/runs/pull",
|
|
373
|
+
json={
|
|
374
|
+
"experiment_id": experiment,
|
|
375
|
+
"limit": MAX_PULL,
|
|
376
|
+
"offset": pulled,
|
|
377
|
+
},
|
|
378
|
+
)
|
|
379
|
+
index, members = _unzip(resp.content)
|
|
380
|
+
write_pull(target, index, members)
|
|
381
|
+
pulled += len(index["runs"])
|
|
382
|
+
if not index["runs"] or pulled >= index["total_matching"]:
|
|
383
|
+
break
|
|
384
|
+
_stamp(target, {"experiment": experiment})
|
|
385
|
+
_report(target)
|
|
386
|
+
return
|
|
387
|
+
if agent is None:
|
|
388
|
+
raise ApiError("pass an agent, or --experiment ID")
|
|
389
|
+
definition = agent_definition(agent)
|
|
390
|
+
ref = definition.ref
|
|
391
|
+
start, end = parse_when(since), parse_when(until)
|
|
392
|
+
filters = _filters(source, start, end)
|
|
393
|
+
wanted = limit if limit is not None else MAX_PULL
|
|
394
|
+
target = out or DEFAULT_ROOT / ref / datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
|
395
|
+
local = driver(definition) is Driver.CLI
|
|
396
|
+
total: int | None = None
|
|
397
|
+
if run_id:
|
|
398
|
+
total = len(run_id)
|
|
399
|
+
if run_id and local:
|
|
400
|
+
rows = [{"run_id": i} for i in run_id]
|
|
401
|
+
write_pull(target, *_pull_local(client, definition, local_harness, rows))
|
|
402
|
+
elif run_id:
|
|
403
|
+
index_rows, members = _server_pull_ids(client, ref, run_id)
|
|
404
|
+
write_pull(
|
|
405
|
+
target,
|
|
406
|
+
{"agent": ref, "pulled_at": datetime.now(UTC).isoformat(), "runs": index_rows},
|
|
407
|
+
members,
|
|
408
|
+
)
|
|
409
|
+
elif local:
|
|
410
|
+
rows, total, note = _local_rows(
|
|
411
|
+
client, definition, local_harness, filters, wanted, offset
|
|
412
|
+
)
|
|
413
|
+
local_index, members = _pull_local(client, definition, local_harness, rows)
|
|
414
|
+
write_pull(target, {**local_index, "note": note}, members)
|
|
415
|
+
else:
|
|
416
|
+
done = 0
|
|
417
|
+
while done < wanted:
|
|
418
|
+
size = min(MAX_PULL, wanted - done)
|
|
419
|
+
resp = client.post(
|
|
420
|
+
f"/api/agents/{encode(ref)}/runs/pull",
|
|
421
|
+
json={**filters, "limit": size, "offset": offset + done},
|
|
422
|
+
)
|
|
423
|
+
index, members = _unzip(resp.content)
|
|
424
|
+
write_pull(target, index, members)
|
|
425
|
+
total = index["total_matching"]
|
|
426
|
+
done += len(index["runs"])
|
|
427
|
+
if len(index["runs"]) < size:
|
|
428
|
+
break
|
|
429
|
+
invocation = {
|
|
430
|
+
**filters,
|
|
431
|
+
"limit": wanted,
|
|
432
|
+
"offset": offset,
|
|
433
|
+
"run_ids": run_id or None,
|
|
434
|
+
}
|
|
435
|
+
_stamp(target, invocation, total)
|
|
436
|
+
_report(target)
|
|
437
|
+
|
|
438
|
+
@runs_app.command("files", help=FILES_HELP)
|
|
439
|
+
def runs_files(
|
|
440
|
+
directory: Path = typer.Argument(..., help="A directory evalkit runs pull wrote."),
|
|
441
|
+
run_id: str = typer.Argument(..., help="A run in its index.json."),
|
|
442
|
+
path: list[str] = typer.Option(
|
|
443
|
+
[], "--path", help="A file, or a folder for everything under it. Repeatable."
|
|
444
|
+
),
|
|
445
|
+
):
|
|
446
|
+
index_path = directory / "index.json"
|
|
447
|
+
if not index_path.is_file():
|
|
448
|
+
raise ApiError(f"{directory} has no index.json; pull runs into it first")
|
|
449
|
+
index = json.loads(index_path.read_text())
|
|
450
|
+
if run_id not in {row["run_id"] for row in index["runs"]}:
|
|
451
|
+
raise ApiError(f"{run_id} is not in {index_path}; pull it first")
|
|
452
|
+
client = client_factory()
|
|
453
|
+
definition = agent_definition(index["agent"])
|
|
454
|
+
files_url = f"/api/agents/{encode(definition.ref)}/runs/{encode(run_id)}/files"
|
|
455
|
+
listing = client.get(files_url).json()["files"]
|
|
456
|
+
stored = {entry["path"] for entry in listing if entry.get("stored")}
|
|
457
|
+
personal = driver(definition) is Driver.CLI
|
|
458
|
+
if personal and not listing:
|
|
459
|
+
listing = asyncio.run(_harness_listing(local_harness(definition), run_id))
|
|
460
|
+
base = f"runs/{safe_dir(run_id)}"
|
|
461
|
+
tree = json.dumps({"files": listing}, indent=2) + "\n"
|
|
462
|
+
write_files(directory, {f"{base}/files.json": tree.encode()})
|
|
463
|
+
if not path:
|
|
464
|
+
for entry in listing:
|
|
465
|
+
size = "-" if entry.get("size") is None else entry["size"]
|
|
466
|
+
typer.echo(f"{size:>12} {entry['path']}")
|
|
467
|
+
typer.echo(f"{len(listing)} file(s); fetch with --path <file or folder>")
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
def save(name: str, data: bytes) -> None:
|
|
471
|
+
write_files(directory, {f"{base}/files/{name}": data})
|
|
472
|
+
|
|
473
|
+
picked = _picked(listing, path)
|
|
474
|
+
from_server = [p for p in picked if p in stored or not personal]
|
|
475
|
+
for name in from_server:
|
|
476
|
+
typer.echo(f"fetching {name}")
|
|
477
|
+
save(name, client.get(f"{files_url}/{quote(name, safe='/')}").content)
|
|
478
|
+
from_harness = [p for p in picked if p not in from_server]
|
|
479
|
+
if from_harness:
|
|
480
|
+
asyncio.run(_harness_fetch(local_harness(definition), run_id, from_harness, save))
|
|
481
|
+
target = (directory / base / "files").resolve()
|
|
482
|
+
typer.echo(f"fetched {len(picked)} file(s) to {target}")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _filters(source: str, since: datetime | None, until: datetime | None) -> dict:
|
|
486
|
+
return {"source": source, "since": _iso(since), "until": _iso(until)}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _report(target: Path) -> None:
|
|
490
|
+
index = json.loads((target / "index.json").read_text())
|
|
491
|
+
failed = sum(1 for row in index["runs"] if row.get("pull_error"))
|
|
492
|
+
typer.echo(f"pulled {len(index['runs'])} run(s) to {target.resolve()}")
|
|
493
|
+
if index.get("note"):
|
|
494
|
+
typer.echo(f"note: {index['note']}")
|
|
495
|
+
if failed:
|
|
496
|
+
typer.echo(f"{failed} run(s) could not be fetched in full; see pull_error in run.json")
|
|
497
|
+
if unignored_in_git(target):
|
|
498
|
+
typer.secho(
|
|
499
|
+
f"warning: {target} is inside a git repo and not ignored; add it to .gitignore",
|
|
500
|
+
fg="yellow",
|
|
501
|
+
err=True,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _picked(listing: list[dict], wanted: list[str]) -> list[str]:
|
|
506
|
+
paths = [entry["path"] for entry in listing]
|
|
507
|
+
picked: list[str] = []
|
|
508
|
+
for want in wanted:
|
|
509
|
+
prefix = want.strip("/")
|
|
510
|
+
if not prefix and not paths:
|
|
511
|
+
raise ApiError("this run has no files to fetch")
|
|
512
|
+
matched = [p for p in paths if not prefix or p == prefix or p.startswith(f"{prefix}/")]
|
|
513
|
+
picked += [p for p in matched or [prefix] if p not in picked]
|
|
514
|
+
return picked
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
async def _harness_listing(harness: HarnessClient, run_id: str) -> list[dict]:
|
|
518
|
+
_require_run_id(run_id)
|
|
519
|
+
try:
|
|
520
|
+
if not (await harness.capabilities()).provides.workspace:
|
|
521
|
+
raise ApiError("this harness does not expose workspace files")
|
|
522
|
+
return (await harness.list_files(run_id)).model_dump()["files"]
|
|
523
|
+
finally:
|
|
524
|
+
await harness.aclose()
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
async def _harness_fetch(
|
|
528
|
+
harness: HarnessClient, run_id: str, paths: list[str], save: Callable[[str, bytes], None]
|
|
529
|
+
) -> None:
|
|
530
|
+
_require_run_id(run_id)
|
|
531
|
+
try:
|
|
532
|
+
for name in paths:
|
|
533
|
+
typer.echo(f"fetching {name}")
|
|
534
|
+
data = await harness.find_file(run_id, name)
|
|
535
|
+
if data is None:
|
|
536
|
+
raise ApiError(f"{name}: no such file on the harness")
|
|
537
|
+
save(name, data)
|
|
538
|
+
finally:
|
|
539
|
+
await harness.aclose()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Server-side CLI command bodies.
|
|
2
|
+
|
|
3
|
+
Everything here needs the `evalkit-py[server]` extra (uvicorn, FastAPI, SQLAlchemy,
|
|
4
|
+
alembic). The base CLI install stays a thin HTTP client; `main.py` imports this
|
|
5
|
+
module only when the server extra is present and points users at it otherwise.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
import psycopg
|
|
11
|
+
import sentry_sdk
|
|
12
|
+
import uvicorn
|
|
13
|
+
from cryptography.fernet import Fernet
|
|
14
|
+
from psycopg import sql
|
|
15
|
+
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
|
16
|
+
from sentry_sdk.integrations.starlette import StarletteIntegration
|
|
17
|
+
from sqlalchemy.engine import make_url
|
|
18
|
+
|
|
19
|
+
from evalkit import version
|
|
20
|
+
from evalkit.api.app import create_app
|
|
21
|
+
from evalkit.cli.client import ApiError
|
|
22
|
+
from evalkit.config import Config
|
|
23
|
+
from evalkit.contracts.settings import JudgeSettings
|
|
24
|
+
from evalkit.db.crypto import SecretBox
|
|
25
|
+
from evalkit.db.llm_providers import LlmProviderStore
|
|
26
|
+
from evalkit.db.rekey import rekey_all
|
|
27
|
+
from evalkit.db.session import make_session_factory, run_migrations
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def init_sentry(config: Config) -> None:
|
|
31
|
+
if config.sentry is None:
|
|
32
|
+
return
|
|
33
|
+
sentry_sdk.init(
|
|
34
|
+
dsn=config.sentry.dsn,
|
|
35
|
+
environment=config.sentry.environment,
|
|
36
|
+
release=f"evalkit@{version.current()}",
|
|
37
|
+
traces_sample_rate=config.sentry.traces_sample_rate,
|
|
38
|
+
send_default_pii=False,
|
|
39
|
+
max_request_body_size=config.sentry.max_request_body_size,
|
|
40
|
+
integrations=[
|
|
41
|
+
StarletteIntegration(failed_request_status_codes={500}),
|
|
42
|
+
FastApiIntegration(failed_request_status_codes={500}),
|
|
43
|
+
],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def serve(config: Config, host: str, port: int) -> None:
|
|
48
|
+
init_sentry(config)
|
|
49
|
+
uvicorn.run(create_app(config), host=host, port=port)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def db_upgrade(config: Config, revision: str) -> None:
|
|
53
|
+
run_migrations(config.db.url, revision)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def rekey(config: Config, old_key: str, new_key: str) -> int:
|
|
57
|
+
factory = make_session_factory(config.db.url)
|
|
58
|
+
count = await rekey_all(factory, SecretBox(old_key), SecretBox(new_key))
|
|
59
|
+
await factory.kw["bind"].dispose()
|
|
60
|
+
return count
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def create_database_if_missing(url: str) -> bool:
|
|
64
|
+
parsed = make_url(url).set(drivername="postgresql")
|
|
65
|
+
if not parsed.database:
|
|
66
|
+
raise ValueError(f"{url} names no database")
|
|
67
|
+
try:
|
|
68
|
+
psycopg.connect(parsed.render_as_string(hide_password=False)).close()
|
|
69
|
+
return False
|
|
70
|
+
except psycopg.OperationalError as exc:
|
|
71
|
+
if f'database "{parsed.database}" does not exist' not in str(exc):
|
|
72
|
+
raise ApiError(
|
|
73
|
+
f"could not connect to Postgres at {parsed.host or 'localhost'}:"
|
|
74
|
+
f"{parsed.port or 5432}: {str(exc).strip()}\n"
|
|
75
|
+
"Is Postgres running (brew services start postgresql@17, sudo systemctl "
|
|
76
|
+
"start postgresql, Postgres.app), and are the user and password in the URL "
|
|
77
|
+
"right? See Try it locally > When something fails."
|
|
78
|
+
) from exc
|
|
79
|
+
admin = parsed.set(database="postgres").render_as_string(hide_password=False)
|
|
80
|
+
with psycopg.connect(admin, autocommit=True) as conn:
|
|
81
|
+
conn.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(parsed.database)))
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def init_database(url: str) -> bool:
|
|
86
|
+
created = create_database_if_missing(url)
|
|
87
|
+
run_migrations(url, "head")
|
|
88
|
+
return created
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
TRY_PROVIDER = "try"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def new_encryption_key() -> str:
|
|
95
|
+
return Fernet.generate_key().decode()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _has_llm_setup(url: str) -> bool:
|
|
99
|
+
factory = make_session_factory(url)
|
|
100
|
+
store = LlmProviderStore(factory)
|
|
101
|
+
providers = await store.list_providers(include_archived=True)
|
|
102
|
+
judge = await store.default_judge()
|
|
103
|
+
await factory.kw["bind"].dispose()
|
|
104
|
+
return bool(providers) or judge is not None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def has_llm_setup(url: str) -> bool:
|
|
108
|
+
return asyncio.run(_has_llm_setup(url))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def _register_default_judge(
|
|
112
|
+
url: str, encryption_key: str, base_url: str, api_key: str, model: str
|
|
113
|
+
) -> None:
|
|
114
|
+
factory = make_session_factory(url)
|
|
115
|
+
store = LlmProviderStore(factory)
|
|
116
|
+
sealed = SecretBox(encryption_key).seal(api_key)
|
|
117
|
+
existing = await store.active_by_base_url(base_url)
|
|
118
|
+
if existing is None:
|
|
119
|
+
await store.create(TRY_PROVIDER, base_url, sealed)
|
|
120
|
+
else:
|
|
121
|
+
await store.edit(existing.name, api_key_encrypted=sealed)
|
|
122
|
+
judge = JudgeSettings.model_validate({"base_url": base_url, "model": model, "concurrency": 4})
|
|
123
|
+
await store.set_default_judge(judge)
|
|
124
|
+
await factory.kw["bind"].dispose()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def register_default_judge(
|
|
128
|
+
url: str, encryption_key: str, base_url: str, api_key: str, model: str
|
|
129
|
+
) -> None:
|
|
130
|
+
asyncio.run(_register_default_judge(url, encryption_key, base_url, api_key, model))
|