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/db/crypto.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from cryptography.fernet import Fernet
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SecretBox:
|
|
5
|
+
def __init__(self, key: str):
|
|
6
|
+
self._fernet = Fernet(key.encode())
|
|
7
|
+
|
|
8
|
+
def seal(self, plain: str) -> bytes:
|
|
9
|
+
return self._fernet.encrypt(plain.encode())
|
|
10
|
+
|
|
11
|
+
def open(self, blob: bytes) -> str:
|
|
12
|
+
return self._fernet.decrypt(blob).decode()
|
evalkit/db/evaluators.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import select
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
5
|
+
|
|
6
|
+
from evalkit.contracts.enums import EvaluatorKind
|
|
7
|
+
from evalkit.contracts.settings import RemoteEvaluatorSettings
|
|
8
|
+
from evalkit.core.remote_evaluators import RemoteEvaluator, UnknownEvaluator
|
|
9
|
+
from evalkit.db import models
|
|
10
|
+
from evalkit.db.locks import lock_name
|
|
11
|
+
from evalkit.db.workspaces import workspace_row
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidEvaluator(ValueError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EvaluatorExists(ValueError):
|
|
19
|
+
def __init__(self, ref: str):
|
|
20
|
+
super().__init__(f"evaluator exists: {ref}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _view(row: models.RemoteEvaluator, workspace: str) -> RemoteEvaluator:
|
|
24
|
+
return RemoteEvaluator(
|
|
25
|
+
name=row.name,
|
|
26
|
+
workspace=workspace,
|
|
27
|
+
kind=EvaluatorKind(row.kind),
|
|
28
|
+
settings=RemoteEvaluatorSettings.model_validate(row.config),
|
|
29
|
+
archived=row.archived_at is not None,
|
|
30
|
+
has_api_key=row.api_key_encrypted is not None,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RemoteEvaluatorStore:
|
|
35
|
+
def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
|
|
36
|
+
self._sessions = session_factory
|
|
37
|
+
|
|
38
|
+
async def _row(self, session: AsyncSession, ref: str) -> models.RemoteEvaluator:
|
|
39
|
+
name, _, workspace = ref.partition("@")
|
|
40
|
+
row = await session.scalar(
|
|
41
|
+
select(models.RemoteEvaluator)
|
|
42
|
+
.join(models.Workspace, models.Workspace.id == models.RemoteEvaluator.workspace_id)
|
|
43
|
+
.where(models.RemoteEvaluator.name == name, models.Workspace.name == workspace)
|
|
44
|
+
)
|
|
45
|
+
if row is None:
|
|
46
|
+
raise UnknownEvaluator(ref)
|
|
47
|
+
return row
|
|
48
|
+
|
|
49
|
+
async def get(self, ref: str) -> RemoteEvaluator:
|
|
50
|
+
async with self._sessions() as session:
|
|
51
|
+
return _view(await self._row(session, ref), ref.partition("@")[2])
|
|
52
|
+
|
|
53
|
+
async def create(
|
|
54
|
+
self,
|
|
55
|
+
workspace: str,
|
|
56
|
+
name: str,
|
|
57
|
+
kind: EvaluatorKind,
|
|
58
|
+
settings: RemoteEvaluatorSettings,
|
|
59
|
+
api_key_encrypted: bytes | None = None,
|
|
60
|
+
) -> RemoteEvaluator:
|
|
61
|
+
if "@" in name or not name:
|
|
62
|
+
raise InvalidEvaluator(f"invalid evaluator name: {name!r}")
|
|
63
|
+
async with self._sessions() as session:
|
|
64
|
+
ws = await workspace_row(session, workspace)
|
|
65
|
+
await lock_name(session, f"evaluator:{ws.id}", name)
|
|
66
|
+
exists = await session.scalar(
|
|
67
|
+
select(models.RemoteEvaluator.id).where(
|
|
68
|
+
models.RemoteEvaluator.workspace_id == ws.id,
|
|
69
|
+
models.RemoteEvaluator.name == name,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if exists is not None:
|
|
73
|
+
raise EvaluatorExists(f"{name}@{workspace}")
|
|
74
|
+
row = models.RemoteEvaluator(
|
|
75
|
+
workspace_id=ws.id,
|
|
76
|
+
name=name,
|
|
77
|
+
kind=kind.value,
|
|
78
|
+
config=settings.model_dump(),
|
|
79
|
+
api_key_encrypted=api_key_encrypted,
|
|
80
|
+
)
|
|
81
|
+
session.add(row)
|
|
82
|
+
await session.commit()
|
|
83
|
+
return _view(row, workspace)
|
|
84
|
+
|
|
85
|
+
async def edit(
|
|
86
|
+
self,
|
|
87
|
+
ref: str,
|
|
88
|
+
settings: RemoteEvaluatorSettings | None = None,
|
|
89
|
+
*,
|
|
90
|
+
api_key_encrypted: bytes | None = None,
|
|
91
|
+
clear_api_key: bool = False,
|
|
92
|
+
) -> RemoteEvaluator:
|
|
93
|
+
async with self._sessions() as session:
|
|
94
|
+
row = await self._row(session, ref)
|
|
95
|
+
if settings is not None:
|
|
96
|
+
if settings.endpoint != row.config["endpoint"]:
|
|
97
|
+
row.api_key_encrypted = None
|
|
98
|
+
row.config = settings.model_dump()
|
|
99
|
+
if api_key_encrypted is not None or clear_api_key:
|
|
100
|
+
row.api_key_encrypted = api_key_encrypted
|
|
101
|
+
await session.commit()
|
|
102
|
+
return _view(row, ref.partition("@")[2])
|
|
103
|
+
|
|
104
|
+
async def restore(self, ref: str) -> RemoteEvaluator:
|
|
105
|
+
async with self._sessions() as session:
|
|
106
|
+
row = await self._row(session, ref)
|
|
107
|
+
row.archived_at = None
|
|
108
|
+
await session.commit()
|
|
109
|
+
return _view(row, ref.partition("@")[2])
|
|
110
|
+
|
|
111
|
+
async def sealed_keys(self, workspace: str) -> dict[str, tuple[str, bytes]]:
|
|
112
|
+
query = (
|
|
113
|
+
select(
|
|
114
|
+
models.RemoteEvaluator.name,
|
|
115
|
+
models.RemoteEvaluator.config,
|
|
116
|
+
models.RemoteEvaluator.api_key_encrypted,
|
|
117
|
+
)
|
|
118
|
+
.join(models.Workspace, models.Workspace.id == models.RemoteEvaluator.workspace_id)
|
|
119
|
+
.where(
|
|
120
|
+
models.Workspace.name == workspace,
|
|
121
|
+
models.RemoteEvaluator.api_key_encrypted.is_not(None),
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
async with self._sessions() as session:
|
|
125
|
+
return {
|
|
126
|
+
name: (config["endpoint"], sealed)
|
|
127
|
+
for name, config, sealed in (await session.execute(query)).all()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async def archive(self, ref: str) -> RemoteEvaluator:
|
|
131
|
+
async with self._sessions() as session:
|
|
132
|
+
row = await self._row(session, ref)
|
|
133
|
+
row.archived_at = datetime.now(UTC)
|
|
134
|
+
await session.commit()
|
|
135
|
+
return _view(row, ref.partition("@")[2])
|
|
136
|
+
|
|
137
|
+
async def in_workspace(self, workspace: str) -> dict[str, RemoteEvaluator]:
|
|
138
|
+
return {e.name: e for e in await self.list_evaluators(workspace=workspace)}
|
|
139
|
+
|
|
140
|
+
async def names(self) -> set[str]:
|
|
141
|
+
return {e.name for e in await self.list_evaluators()}
|
|
142
|
+
|
|
143
|
+
async def list_evaluators(
|
|
144
|
+
self, workspace: str | None = None, include_archived: bool = False
|
|
145
|
+
) -> list[RemoteEvaluator]:
|
|
146
|
+
query = (
|
|
147
|
+
select(models.RemoteEvaluator, models.Workspace.name)
|
|
148
|
+
.join(models.Workspace, models.Workspace.id == models.RemoteEvaluator.workspace_id)
|
|
149
|
+
.order_by(models.Workspace.name, models.RemoteEvaluator.name)
|
|
150
|
+
)
|
|
151
|
+
if workspace is not None:
|
|
152
|
+
query = query.where(models.Workspace.name == workspace)
|
|
153
|
+
if not include_archived:
|
|
154
|
+
query = query.where(models.RemoteEvaluator.archived_at.is_(None))
|
|
155
|
+
async with self._sessions() as session:
|
|
156
|
+
return [_view(row, ws) for row, ws in (await session.execute(query)).all()]
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from datetime import UTC, datetime, timedelta
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import Select, func, select, update
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
5
|
+
from sqlalchemy.orm import aliased
|
|
6
|
+
|
|
7
|
+
from evalkit.contracts.enums import ExperimentStatus, RunStatus
|
|
8
|
+
from evalkit.core.access import WorkspaceRef, experiment_workspace
|
|
9
|
+
from evalkit.core.runner import ACTIVE, ALLOWED, LEASE_SECONDS
|
|
10
|
+
from evalkit.core.workspace import parse_slug
|
|
11
|
+
from evalkit.db import models
|
|
12
|
+
from evalkit.db.cases import case_views, dataset_by_ref
|
|
13
|
+
from evalkit.db.workspaces import to_ref, workspace_row
|
|
14
|
+
from evalkit.store.base import (
|
|
15
|
+
CaseView,
|
|
16
|
+
Conflict,
|
|
17
|
+
Experiment,
|
|
18
|
+
ExperimentConfig,
|
|
19
|
+
NewExperiment,
|
|
20
|
+
UnknownExperiment,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
LEASE = timedelta(seconds=LEASE_SECONDS)
|
|
24
|
+
HomeWorkspace = aliased(models.Workspace)
|
|
25
|
+
DatasetWorkspace = aliased(models.Workspace)
|
|
26
|
+
AgentWorkspace = aliased(models.Workspace)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _query() -> Select:
|
|
30
|
+
return (
|
|
31
|
+
select(
|
|
32
|
+
models.Experiment,
|
|
33
|
+
HomeWorkspace.name,
|
|
34
|
+
models.Dataset.name,
|
|
35
|
+
DatasetWorkspace.name,
|
|
36
|
+
models.Agent.name,
|
|
37
|
+
AgentWorkspace.name,
|
|
38
|
+
models.User.username,
|
|
39
|
+
)
|
|
40
|
+
.join(HomeWorkspace, HomeWorkspace.id == models.Experiment.workspace_id)
|
|
41
|
+
.join(models.Dataset, models.Dataset.id == models.Experiment.dataset_id)
|
|
42
|
+
.join(DatasetWorkspace, DatasetWorkspace.id == models.Dataset.workspace_id)
|
|
43
|
+
.join(models.Agent, models.Agent.id == models.Experiment.agent_id)
|
|
44
|
+
.join(AgentWorkspace, AgentWorkspace.id == models.Agent.workspace_id)
|
|
45
|
+
.join(models.User, models.User.id == models.Experiment.created_by)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _to_experiment(row) -> Experiment:
|
|
50
|
+
exp, home, dataset, dataset_ws, agent, agent_ws, created_by = row
|
|
51
|
+
return Experiment(
|
|
52
|
+
id=exp.id,
|
|
53
|
+
number=exp.number,
|
|
54
|
+
workspace=home,
|
|
55
|
+
dataset=f"{dataset}@{dataset_ws}",
|
|
56
|
+
agent=f"{agent}@{agent_ws}",
|
|
57
|
+
agent_id=exp.agent_id,
|
|
58
|
+
created_by=created_by,
|
|
59
|
+
status=ExperimentStatus(exp.status),
|
|
60
|
+
status_error=exp.status_error,
|
|
61
|
+
lease_holder=exp.lease_holder,
|
|
62
|
+
lease_expires_at=exp.lease_expires_at,
|
|
63
|
+
repetitions=exp.repetitions,
|
|
64
|
+
config=ExperimentConfig.model_validate(exp.config),
|
|
65
|
+
started_at=exp.started_at,
|
|
66
|
+
ended_at=exp.ended_at,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def get_experiment(session: AsyncSession, exp_id: int) -> Experiment:
|
|
71
|
+
row = (await session.execute(_query().where(models.Experiment.id == exp_id))).one_or_none()
|
|
72
|
+
if row is None:
|
|
73
|
+
raise UnknownExperiment(exp_id)
|
|
74
|
+
return _to_experiment(row)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def list_experiments(
|
|
78
|
+
session: AsyncSession,
|
|
79
|
+
*,
|
|
80
|
+
dataset_id: int | None = None,
|
|
81
|
+
workspace_id: int | None = None,
|
|
82
|
+
status: ExperimentStatus | None = None,
|
|
83
|
+
agent_id: int | None = None,
|
|
84
|
+
ran_case_ids: list[str] | None = None,
|
|
85
|
+
limit: int = 50,
|
|
86
|
+
offset: int = 0,
|
|
87
|
+
) -> list[Experiment]:
|
|
88
|
+
query = _query()
|
|
89
|
+
if ran_case_ids is not None:
|
|
90
|
+
query = query.where(
|
|
91
|
+
select(models.ExperimentRun.id)
|
|
92
|
+
.join(models.Case, models.Case.id == models.ExperimentRun.case_ref)
|
|
93
|
+
.where(
|
|
94
|
+
models.ExperimentRun.experiment_id == models.Experiment.id,
|
|
95
|
+
models.Case.case_id.in_(ran_case_ids),
|
|
96
|
+
)
|
|
97
|
+
.exists()
|
|
98
|
+
)
|
|
99
|
+
if dataset_id is not None:
|
|
100
|
+
query = query.where(models.Experiment.dataset_id == dataset_id)
|
|
101
|
+
if workspace_id is not None:
|
|
102
|
+
query = query.where(models.Experiment.workspace_id == workspace_id)
|
|
103
|
+
if status is not None:
|
|
104
|
+
query = query.where(models.Experiment.status == status.value)
|
|
105
|
+
if agent_id is not None:
|
|
106
|
+
query = query.where(models.Experiment.agent_id == agent_id)
|
|
107
|
+
rows = await session.execute(
|
|
108
|
+
query.order_by(models.Experiment.id.desc()).limit(limit).offset(offset)
|
|
109
|
+
)
|
|
110
|
+
return [_to_experiment(row) for row in rows.all()]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def _agent(session: AsyncSession, ref: str) -> tuple[models.Agent, WorkspaceRef]:
|
|
114
|
+
parsed = parse_slug(ref)
|
|
115
|
+
workspace = await workspace_row(session, parsed.workspace)
|
|
116
|
+
agent = await session.scalar(
|
|
117
|
+
select(models.Agent).where(
|
|
118
|
+
models.Agent.workspace_id == workspace.id, models.Agent.name == parsed.name
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
if agent is None:
|
|
122
|
+
raise KeyError(f"unknown agent: {ref}")
|
|
123
|
+
return agent, to_ref(workspace)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def _personal(session: AsyncSession, username: str) -> tuple[int, WorkspaceRef]:
|
|
127
|
+
user = await session.scalar(select(models.User).where(models.User.username == username))
|
|
128
|
+
if user is None:
|
|
129
|
+
raise KeyError(f"unknown user: {username}")
|
|
130
|
+
workspace = await session.scalar(
|
|
131
|
+
select(models.Workspace).where(models.Workspace.owner_user_id == user.id)
|
|
132
|
+
)
|
|
133
|
+
assert workspace is not None
|
|
134
|
+
return user.id, to_ref(workspace)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def create_experiment(session: AsyncSession, req: NewExperiment) -> Experiment:
|
|
138
|
+
dataset = await dataset_by_ref(session, req.dataset)
|
|
139
|
+
dataset_row = await session.scalar(
|
|
140
|
+
select(models.Dataset)
|
|
141
|
+
.where(models.Dataset.id == dataset.id)
|
|
142
|
+
.with_for_update(key_share=True)
|
|
143
|
+
)
|
|
144
|
+
assert dataset_row is not None
|
|
145
|
+
dataset_ws = to_ref(await workspace_row(session, dataset.workspace))
|
|
146
|
+
agent, agent_ws = await _agent(session, req.agent)
|
|
147
|
+
runner_id, runner_ws = await _personal(session, req.runner)
|
|
148
|
+
home = experiment_workspace(runner_ws, dataset_ws, agent_ws)
|
|
149
|
+
number = await session.scalar(
|
|
150
|
+
select(func.coalesce(func.max(models.Experiment.number), 0) + 1).where(
|
|
151
|
+
models.Experiment.dataset_id == dataset.id
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
exp = models.Experiment(
|
|
155
|
+
workspace_id=home.id,
|
|
156
|
+
dataset_id=dataset.id,
|
|
157
|
+
number=number,
|
|
158
|
+
agent_id=agent.id,
|
|
159
|
+
created_by=runner_id,
|
|
160
|
+
status=ExperimentStatus.RUNNING.value,
|
|
161
|
+
lease_holder=req.holder,
|
|
162
|
+
lease_expires_at=func.now() + LEASE,
|
|
163
|
+
repetitions=req.repetitions,
|
|
164
|
+
config=req.config.model_dump(mode="json"),
|
|
165
|
+
)
|
|
166
|
+
session.add(exp)
|
|
167
|
+
await session.flush()
|
|
168
|
+
views = await case_views(
|
|
169
|
+
session,
|
|
170
|
+
dataset_row.workspace_id,
|
|
171
|
+
[(pinned.case_ref, pinned.content_hash) for pinned in req.cases],
|
|
172
|
+
agent=req.agent,
|
|
173
|
+
dataset_id=dataset.id,
|
|
174
|
+
)
|
|
175
|
+
session.add_all(
|
|
176
|
+
models.ExperimentCase(
|
|
177
|
+
experiment_id=exp.id,
|
|
178
|
+
case_ref=view.case_ref,
|
|
179
|
+
content_hash=view.content_hash,
|
|
180
|
+
effective_evaluators=view.effective_evaluators,
|
|
181
|
+
)
|
|
182
|
+
for view in views
|
|
183
|
+
)
|
|
184
|
+
await session.flush()
|
|
185
|
+
return await get_experiment(session, exp.id)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def _compare_and_set(
|
|
189
|
+
session: AsyncSession,
|
|
190
|
+
exp_id: int,
|
|
191
|
+
to: ExperimentStatus,
|
|
192
|
+
holder: str | None,
|
|
193
|
+
error: str | None,
|
|
194
|
+
) -> bool:
|
|
195
|
+
allowed_from = [f.value for f, t in ALLOWED if t == to and f is not None]
|
|
196
|
+
active = [s.value for s in ACTIVE]
|
|
197
|
+
live = to in ACTIVE
|
|
198
|
+
result = await session.execute(
|
|
199
|
+
update(models.Experiment)
|
|
200
|
+
.where(
|
|
201
|
+
models.Experiment.id == exp_id,
|
|
202
|
+
models.Experiment.status.in_(allowed_from),
|
|
203
|
+
models.Experiment.status.not_in(active)
|
|
204
|
+
| (models.Experiment.lease_expires_at < func.now())
|
|
205
|
+
| (models.Experiment.lease_holder == holder),
|
|
206
|
+
)
|
|
207
|
+
.values(
|
|
208
|
+
status=to.value,
|
|
209
|
+
status_error=error,
|
|
210
|
+
lease_holder=holder if live else None,
|
|
211
|
+
lease_expires_at=func.now() + LEASE if live else None,
|
|
212
|
+
ended_at=None if live else func.now(),
|
|
213
|
+
updated_at=func.now(),
|
|
214
|
+
)
|
|
215
|
+
.execution_options(synchronize_session=False)
|
|
216
|
+
)
|
|
217
|
+
return result.rowcount == 1 # type: ignore[attr-defined]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def _has_pending_runs(session: AsyncSession, exp_id: int) -> bool:
|
|
221
|
+
exp = await session.get(models.Experiment, exp_id)
|
|
222
|
+
if exp is None:
|
|
223
|
+
raise UnknownExperiment(exp_id)
|
|
224
|
+
completed = (
|
|
225
|
+
select(func.count())
|
|
226
|
+
.select_from(models.ExperimentRun)
|
|
227
|
+
.join(models.AgentRun, models.AgentRun.id == models.ExperimentRun.agent_run_id)
|
|
228
|
+
.where(
|
|
229
|
+
models.ExperimentRun.experiment_id == exp_id,
|
|
230
|
+
models.AgentRun.status == RunStatus.COMPLETED.value,
|
|
231
|
+
)
|
|
232
|
+
.scalar_subquery()
|
|
233
|
+
)
|
|
234
|
+
pinned = (
|
|
235
|
+
select(func.count())
|
|
236
|
+
.select_from(models.ExperimentCase)
|
|
237
|
+
.where(models.ExperimentCase.experiment_id == exp_id)
|
|
238
|
+
.scalar_subquery()
|
|
239
|
+
)
|
|
240
|
+
found = (await session.execute(select(pinned, completed))).one()
|
|
241
|
+
return found[0] * exp.repetitions > found[1]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def has_pending_runs(session: AsyncSession, exp_id: int) -> bool:
|
|
245
|
+
return await _has_pending_runs(session, exp_id)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def resumable(exp: Experiment, pending_runs: bool) -> bool:
|
|
249
|
+
if exp.status is ExperimentStatus.FAILED:
|
|
250
|
+
return True
|
|
251
|
+
return exp.status is ExperimentStatus.COMPLETED and pending_runs
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def transition(
|
|
255
|
+
session: AsyncSession,
|
|
256
|
+
exp_id: int,
|
|
257
|
+
to: ExperimentStatus,
|
|
258
|
+
holder: str | None,
|
|
259
|
+
error: str | None = None,
|
|
260
|
+
) -> Experiment:
|
|
261
|
+
current = await get_experiment(session, exp_id)
|
|
262
|
+
if current.status is to and (
|
|
263
|
+
current.lease_holder == holder if to in ACTIVE else current.status_error == error
|
|
264
|
+
):
|
|
265
|
+
return current
|
|
266
|
+
if (
|
|
267
|
+
current.status is ExperimentStatus.COMPLETED
|
|
268
|
+
and to is ExperimentStatus.RUNNING
|
|
269
|
+
and not await _has_pending_runs(session, exp_id)
|
|
270
|
+
):
|
|
271
|
+
raise Conflict(f"experiment {exp_id} has no runs left to resume")
|
|
272
|
+
if not await _compare_and_set(session, exp_id, to, holder, error):
|
|
273
|
+
current = await get_experiment(session, exp_id)
|
|
274
|
+
raise Conflict(
|
|
275
|
+
f"experiment is {current.status.value}, held by {current.lease_holder or 'nobody'}"
|
|
276
|
+
)
|
|
277
|
+
session.expire_all()
|
|
278
|
+
return await get_experiment(session, exp_id)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
async def transition_stale(session: AsyncSession, exp_id: int) -> bool:
|
|
282
|
+
holder = await session.scalar(
|
|
283
|
+
select(models.Experiment.lease_holder).where(models.Experiment.id == exp_id)
|
|
284
|
+
)
|
|
285
|
+
return await _compare_and_set(
|
|
286
|
+
session,
|
|
287
|
+
exp_id,
|
|
288
|
+
ExperimentStatus.FAILED,
|
|
289
|
+
None,
|
|
290
|
+
f"runner {holder} stopped responding",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def renew_lease(session: AsyncSession, exp_id: int, holder: str) -> bool:
|
|
295
|
+
result = await session.execute(
|
|
296
|
+
update(models.Experiment)
|
|
297
|
+
.where(models.Experiment.id == exp_id, models.Experiment.lease_holder == holder)
|
|
298
|
+
.values(lease_expires_at=func.now() + LEASE)
|
|
299
|
+
.execution_options(synchronize_session=False)
|
|
300
|
+
)
|
|
301
|
+
return result.rowcount == 1 # type: ignore[attr-defined]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def stale_active(session: AsyncSession, now: datetime) -> list[int]:
|
|
305
|
+
rows = await session.scalars(
|
|
306
|
+
select(models.Experiment.id)
|
|
307
|
+
.where(
|
|
308
|
+
models.Experiment.status.in_([s.value for s in ACTIVE]),
|
|
309
|
+
models.Experiment.lease_expires_at < now,
|
|
310
|
+
)
|
|
311
|
+
.order_by(models.Experiment.id)
|
|
312
|
+
)
|
|
313
|
+
return list(rows.all())
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
async def set_model(session: AsyncSession, exp_id: int, model: str) -> None:
|
|
317
|
+
row = await session.get(models.Experiment, exp_id)
|
|
318
|
+
if row is None:
|
|
319
|
+
raise UnknownExperiment(exp_id)
|
|
320
|
+
row.config = {**row.config, "model": model}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def pinned_cases(session: AsyncSession, exp_id: int) -> list[CaseView]:
|
|
324
|
+
exp = await session.get(models.Experiment, exp_id)
|
|
325
|
+
if exp is None:
|
|
326
|
+
raise UnknownExperiment(exp_id)
|
|
327
|
+
pinned = (
|
|
328
|
+
await session.scalars(
|
|
329
|
+
select(models.ExperimentCase).where(models.ExperimentCase.experiment_id == exp_id)
|
|
330
|
+
)
|
|
331
|
+
).all()
|
|
332
|
+
dataset = await session.get(models.Dataset, exp.dataset_id)
|
|
333
|
+
assert dataset is not None
|
|
334
|
+
views = await case_views(
|
|
335
|
+
session,
|
|
336
|
+
dataset.workspace_id,
|
|
337
|
+
[(row.case_ref, row.content_hash) for row in pinned],
|
|
338
|
+
agent=None,
|
|
339
|
+
dataset_id=dataset.id,
|
|
340
|
+
)
|
|
341
|
+
frozen = {row.case_ref: row.effective_evaluators for row in pinned}
|
|
342
|
+
return [
|
|
343
|
+
view.model_copy(update={"effective_evaluators": frozen[view.case_ref]}) for view in views
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
async def reap(session_factory: async_sessionmaker[AsyncSession]) -> list[int]:
|
|
348
|
+
async with session_factory() as s:
|
|
349
|
+
reaped = [
|
|
350
|
+
exp_id
|
|
351
|
+
for exp_id in await stale_active(s, datetime.now(UTC))
|
|
352
|
+
if await transition_stale(s, exp_id)
|
|
353
|
+
]
|
|
354
|
+
await s.commit()
|
|
355
|
+
return reaped
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
async def experiment_of_run(session: AsyncSession, experiment_run_id: int) -> int | None:
|
|
359
|
+
return await session.scalar(
|
|
360
|
+
select(models.ExperimentRun.experiment_id).where(
|
|
361
|
+
models.ExperimentRun.id == experiment_run_id
|
|
362
|
+
)
|
|
363
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from sqlalchemy import Select, func, select
|
|
2
|
+
from sqlalchemy.dialects.postgresql import insert
|
|
3
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
4
|
+
|
|
5
|
+
from evalkit.db import models
|
|
6
|
+
from evalkit.store.base import FixtureUse, FixtureUsePage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def known_blobs(session: AsyncSession, shas: list[str]) -> set[str]:
|
|
10
|
+
if not shas:
|
|
11
|
+
return set()
|
|
12
|
+
rows = await session.scalars(
|
|
13
|
+
select(models.FixtureBlob.sha256).where(models.FixtureBlob.sha256.in_(shas))
|
|
14
|
+
)
|
|
15
|
+
return set(rows.all())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def blob_size(session: AsyncSession, sha256: str) -> int | None:
|
|
19
|
+
return await session.scalar(
|
|
20
|
+
select(models.FixtureBlob.size).where(models.FixtureBlob.sha256 == sha256)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def record_blob(
|
|
25
|
+
session: AsyncSession, sha256: str, size: int, uploaded_by: int | None
|
|
26
|
+
) -> None:
|
|
27
|
+
await session.execute(
|
|
28
|
+
insert(models.FixtureBlob)
|
|
29
|
+
.values(sha256=sha256, size=size, uploaded_by=uploaded_by)
|
|
30
|
+
.on_conflict_do_nothing(index_elements=["sha256"])
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _uses(workspace_id: int, text: str | None) -> Select:
|
|
35
|
+
query = (
|
|
36
|
+
select(
|
|
37
|
+
models.CaseVersionFile.key,
|
|
38
|
+
models.CaseVersionFile.sha256,
|
|
39
|
+
models.FixtureBlob.size,
|
|
40
|
+
func.array_agg(func.distinct(models.Dataset.name)).label("datasets"),
|
|
41
|
+
models.User.username,
|
|
42
|
+
)
|
|
43
|
+
.join(
|
|
44
|
+
models.DatasetCase,
|
|
45
|
+
(models.DatasetCase.case_ref == models.CaseVersionFile.case_ref)
|
|
46
|
+
& (models.DatasetCase.content_hash == models.CaseVersionFile.content_hash),
|
|
47
|
+
)
|
|
48
|
+
.join(models.Dataset, models.Dataset.id == models.DatasetCase.dataset_id)
|
|
49
|
+
.join(models.FixtureBlob, models.FixtureBlob.sha256 == models.CaseVersionFile.sha256)
|
|
50
|
+
.outerjoin(models.User, models.User.id == models.FixtureBlob.uploaded_by)
|
|
51
|
+
.where(models.Dataset.workspace_id == workspace_id, models.DatasetCase.removed_at.is_(None))
|
|
52
|
+
.group_by(
|
|
53
|
+
models.CaseVersionFile.key,
|
|
54
|
+
models.CaseVersionFile.sha256,
|
|
55
|
+
models.FixtureBlob.size,
|
|
56
|
+
models.User.username,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
if text:
|
|
60
|
+
query = query.where(models.CaseVersionFile.key.icontains(text, autoescape=True))
|
|
61
|
+
return query
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def workspace_fixtures(
|
|
65
|
+
session: AsyncSession,
|
|
66
|
+
workspace_id: int,
|
|
67
|
+
*,
|
|
68
|
+
text: str | None = None,
|
|
69
|
+
limit: int = 50,
|
|
70
|
+
offset: int = 0,
|
|
71
|
+
) -> FixtureUsePage:
|
|
72
|
+
uses = _uses(workspace_id, text)
|
|
73
|
+
total = await session.scalar(select(func.count()).select_from(uses.subquery()))
|
|
74
|
+
rows = await session.execute(
|
|
75
|
+
uses.order_by(models.CaseVersionFile.key, models.CaseVersionFile.sha256)
|
|
76
|
+
.limit(limit)
|
|
77
|
+
.offset(offset)
|
|
78
|
+
)
|
|
79
|
+
return FixtureUsePage(
|
|
80
|
+
items=[
|
|
81
|
+
FixtureUse(
|
|
82
|
+
key=key,
|
|
83
|
+
sha256=sha256,
|
|
84
|
+
size=size,
|
|
85
|
+
datasets=sorted(datasets),
|
|
86
|
+
uploaded_by=uploaded_by,
|
|
87
|
+
)
|
|
88
|
+
for key, sha256, size, datasets, uploaded_by in rows.all()
|
|
89
|
+
],
|
|
90
|
+
total=total or 0,
|
|
91
|
+
)
|
evalkit/db/keys.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
2
|
+
|
|
3
|
+
from evalkit.core.agents import AgentDefinition
|
|
4
|
+
from evalkit.credentials import Kind
|
|
5
|
+
from evalkit.db.agents import AgentStore
|
|
6
|
+
from evalkit.db.crypto import SecretBox
|
|
7
|
+
from evalkit.db.evaluators import RemoteEvaluatorStore
|
|
8
|
+
from evalkit.db.llm_providers import LlmProviderStore
|
|
9
|
+
from evalkit.db.trace_sources import EncryptionKeyMissing
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StoredKeys:
|
|
13
|
+
def __init__(self, keys: dict[tuple[Kind, str], tuple[str, str]]):
|
|
14
|
+
self._keys = keys
|
|
15
|
+
|
|
16
|
+
def key_for(self, kind: Kind, ref: str, url: str | None = None) -> str | None:
|
|
17
|
+
stored = self._keys.get((kind, ref))
|
|
18
|
+
if stored is None:
|
|
19
|
+
return None
|
|
20
|
+
key, stored_url = stored
|
|
21
|
+
return key if url is None or url == stored_url else None
|
|
22
|
+
|
|
23
|
+
def require_judge_key(self, base_url: str) -> str | None:
|
|
24
|
+
return self.key_for("llm_provider", base_url)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NoKeysLoaded:
|
|
28
|
+
def key_for(self, kind: Kind, ref: str, url: str | None = None) -> str | None:
|
|
29
|
+
raise RuntimeError("server keys are loaded per experiment with KeyLoader.load")
|
|
30
|
+
|
|
31
|
+
def require_judge_key(self, base_url: str) -> str | None:
|
|
32
|
+
raise RuntimeError("server keys are loaded per experiment with KeyLoader.load")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class KeyLoader:
|
|
36
|
+
def __init__(self, session_factory: async_sessionmaker[AsyncSession], box: SecretBox | None):
|
|
37
|
+
self._agents = AgentStore(session_factory)
|
|
38
|
+
self._evaluators = RemoteEvaluatorStore(session_factory)
|
|
39
|
+
self._providers = LlmProviderStore(session_factory)
|
|
40
|
+
self._box = box
|
|
41
|
+
|
|
42
|
+
def _open(self, sealed: bytes) -> str:
|
|
43
|
+
if self._box is None:
|
|
44
|
+
raise EncryptionKeyMissing()
|
|
45
|
+
return self._box.open(sealed)
|
|
46
|
+
|
|
47
|
+
def seal(self, plain: str | None) -> bytes | None:
|
|
48
|
+
if not plain:
|
|
49
|
+
return None
|
|
50
|
+
if self._box is None:
|
|
51
|
+
raise EncryptionKeyMissing()
|
|
52
|
+
return self._box.seal(plain)
|
|
53
|
+
|
|
54
|
+
async def agent_key(self, agent: AgentDefinition) -> str | None:
|
|
55
|
+
if not agent.workspace_is_shared:
|
|
56
|
+
return None
|
|
57
|
+
sealed = await self._agents.sealed_key(agent.ref)
|
|
58
|
+
return self._open(sealed) if sealed is not None else None
|
|
59
|
+
|
|
60
|
+
async def load(self, agent: AgentDefinition, judge_base_url: str | None) -> StoredKeys:
|
|
61
|
+
keys: dict[tuple[Kind, str], tuple[str, str]] = {}
|
|
62
|
+
if not agent.workspace_is_shared:
|
|
63
|
+
return StoredKeys(keys)
|
|
64
|
+
sealed_agent = await self._agents.sealed_key(agent.ref)
|
|
65
|
+
if sealed_agent is not None and agent.harness_url is not None:
|
|
66
|
+
keys["agent", agent.ref] = (self._open(sealed_agent), agent.harness_url)
|
|
67
|
+
for name, (endpoint, sealed) in (
|
|
68
|
+
await self._evaluators.sealed_keys(agent.workspace)
|
|
69
|
+
).items():
|
|
70
|
+
keys["evaluator", f"{name}@{agent.workspace}"] = (self._open(sealed), endpoint)
|
|
71
|
+
if judge_base_url is not None:
|
|
72
|
+
sealed_provider = await self._providers.sealed_key(judge_base_url)
|
|
73
|
+
if sealed_provider is not None:
|
|
74
|
+
keys["llm_provider", judge_base_url] = (
|
|
75
|
+
self._open(sealed_provider),
|
|
76
|
+
judge_base_url,
|
|
77
|
+
)
|
|
78
|
+
return StoredKeys(keys)
|