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,308 @@
|
|
|
1
|
+
from datetime import UTC, datetime, timedelta
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from sqlalchemy import func, select, update
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
6
|
+
|
|
7
|
+
from evalkit.contracts.enums import TraceImportStatus
|
|
8
|
+
from evalkit.core.traces.importer import (
|
|
9
|
+
ACTIVE_IMPORT,
|
|
10
|
+
IMPORT_LEASE_SECONDS,
|
|
11
|
+
IMPORT_STOPPED_ERROR,
|
|
12
|
+
ImportJob,
|
|
13
|
+
)
|
|
14
|
+
from evalkit.db import models
|
|
15
|
+
from evalkit.db.locks import lock_name
|
|
16
|
+
from evalkit.store.base import Conflict
|
|
17
|
+
|
|
18
|
+
ACTIVE = [s.value for s in ACTIVE_IMPORT]
|
|
19
|
+
RESTARTABLE = [TraceImportStatus.FAILED.value, TraceImportStatus.STOPPED.value]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnknownTraceImport(KeyError):
|
|
23
|
+
def __init__(self, import_id: int):
|
|
24
|
+
super().__init__(f"unknown trace import: {import_id}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ImportRunning(Conflict):
|
|
28
|
+
def __init__(self, import_id: int):
|
|
29
|
+
super().__init__(f"import {import_id} is already running for this agent")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ImportNotActive(Conflict):
|
|
33
|
+
def __init__(self, import_id: int, status: str, action: str):
|
|
34
|
+
super().__init__(f"import {import_id} is {status}; only {action}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TraceImportView(BaseModel):
|
|
38
|
+
id: int
|
|
39
|
+
agent: str
|
|
40
|
+
source: str
|
|
41
|
+
project: str
|
|
42
|
+
since: datetime
|
|
43
|
+
until: datetime
|
|
44
|
+
resume_from: datetime
|
|
45
|
+
status: TraceImportStatus
|
|
46
|
+
traces: int
|
|
47
|
+
spans: int
|
|
48
|
+
sample_session: str | None
|
|
49
|
+
error: str | None
|
|
50
|
+
created_by: str
|
|
51
|
+
created_at: datetime
|
|
52
|
+
finished_at: datetime | None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _lease() -> datetime:
|
|
56
|
+
return datetime.now(UTC) + timedelta(seconds=IMPORT_LEASE_SECONDS)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _views():
|
|
60
|
+
return (
|
|
61
|
+
select(
|
|
62
|
+
models.TraceImport,
|
|
63
|
+
models.Agent.name,
|
|
64
|
+
models.Workspace.name,
|
|
65
|
+
models.TraceSource.name,
|
|
66
|
+
models.User.username,
|
|
67
|
+
)
|
|
68
|
+
.join(models.Agent, models.Agent.id == models.TraceImport.agent_id)
|
|
69
|
+
.join(models.Workspace, models.Workspace.id == models.Agent.workspace_id)
|
|
70
|
+
.join(models.TraceSource, models.TraceSource.id == models.TraceImport.trace_source_id)
|
|
71
|
+
.join(models.User, models.User.id == models.TraceImport.created_by)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _view(
|
|
76
|
+
row: models.TraceImport, agent: str, workspace: str, source: str, user: str
|
|
77
|
+
) -> TraceImportView:
|
|
78
|
+
return TraceImportView(
|
|
79
|
+
id=row.id,
|
|
80
|
+
agent=f"{agent}@{workspace}",
|
|
81
|
+
source=source,
|
|
82
|
+
project=row.project,
|
|
83
|
+
since=row.since,
|
|
84
|
+
until=row.until,
|
|
85
|
+
resume_from=row.resume_from,
|
|
86
|
+
status=TraceImportStatus(row.status),
|
|
87
|
+
traces=row.traces,
|
|
88
|
+
spans=row.spans,
|
|
89
|
+
sample_session=row.sample_session,
|
|
90
|
+
error=row.error,
|
|
91
|
+
created_by=user,
|
|
92
|
+
created_at=row.created_at,
|
|
93
|
+
finished_at=row.finished_at,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def _refuse_second(session: AsyncSession, agent_id: int) -> None:
|
|
98
|
+
await lock_name(session, "trace-import", str(agent_id))
|
|
99
|
+
running = await session.scalar(
|
|
100
|
+
select(models.TraceImport.id).where(
|
|
101
|
+
models.TraceImport.agent_id == agent_id, models.TraceImport.status.in_(ACTIVE)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
if running is not None:
|
|
105
|
+
raise ImportRunning(running)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TraceImportStore:
|
|
109
|
+
def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
|
|
110
|
+
self._sessions = session_factory
|
|
111
|
+
|
|
112
|
+
async def _one(self, session: AsyncSession, agent_id: int, import_id: int) -> TraceImportView:
|
|
113
|
+
found = (
|
|
114
|
+
await session.execute(
|
|
115
|
+
_views().where(
|
|
116
|
+
models.TraceImport.id == import_id, models.TraceImport.agent_id == agent_id
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
).one_or_none()
|
|
120
|
+
if found is None:
|
|
121
|
+
raise UnknownTraceImport(import_id)
|
|
122
|
+
return _view(*found)
|
|
123
|
+
|
|
124
|
+
async def create(
|
|
125
|
+
self,
|
|
126
|
+
agent_id: int,
|
|
127
|
+
trace_source_id: int,
|
|
128
|
+
project: str,
|
|
129
|
+
since: datetime,
|
|
130
|
+
until: datetime,
|
|
131
|
+
created_by: int,
|
|
132
|
+
holder: str,
|
|
133
|
+
) -> TraceImportView:
|
|
134
|
+
async with self._sessions() as session:
|
|
135
|
+
await _refuse_second(session, agent_id)
|
|
136
|
+
row = models.TraceImport(
|
|
137
|
+
agent_id=agent_id,
|
|
138
|
+
trace_source_id=trace_source_id,
|
|
139
|
+
project=project,
|
|
140
|
+
since=since,
|
|
141
|
+
until=until,
|
|
142
|
+
resume_from=since,
|
|
143
|
+
resume_trace_ids=[],
|
|
144
|
+
status=TraceImportStatus.RUNNING.value,
|
|
145
|
+
traces=0,
|
|
146
|
+
spans=0,
|
|
147
|
+
lease_holder=holder,
|
|
148
|
+
lease_expires_at=_lease(),
|
|
149
|
+
created_by=created_by,
|
|
150
|
+
)
|
|
151
|
+
session.add(row)
|
|
152
|
+
await session.commit()
|
|
153
|
+
return await self._one(session, agent_id, row.id)
|
|
154
|
+
|
|
155
|
+
async def get(self, agent_id: int, import_id: int) -> TraceImportView:
|
|
156
|
+
async with self._sessions() as session:
|
|
157
|
+
return await self._one(session, agent_id, import_id)
|
|
158
|
+
|
|
159
|
+
async def recent(self, agent_id: int, limit: int = 20) -> list[TraceImportView]:
|
|
160
|
+
async with self._sessions() as session:
|
|
161
|
+
rows = await session.execute(
|
|
162
|
+
_views()
|
|
163
|
+
.where(models.TraceImport.agent_id == agent_id)
|
|
164
|
+
.order_by(models.TraceImport.id.desc())
|
|
165
|
+
.limit(limit)
|
|
166
|
+
)
|
|
167
|
+
return [_view(*row) for row in rows.all()]
|
|
168
|
+
|
|
169
|
+
async def stop(self, agent_id: int, import_id: int) -> TraceImportView:
|
|
170
|
+
async with self._sessions() as session:
|
|
171
|
+
current = await self._one(session, agent_id, import_id)
|
|
172
|
+
if current.status is not TraceImportStatus.RUNNING:
|
|
173
|
+
raise ImportNotActive(import_id, current.status, "a running import can be stopped")
|
|
174
|
+
await session.execute(
|
|
175
|
+
update(models.TraceImport)
|
|
176
|
+
.where(
|
|
177
|
+
models.TraceImport.id == import_id,
|
|
178
|
+
models.TraceImport.status == TraceImportStatus.RUNNING.value,
|
|
179
|
+
)
|
|
180
|
+
.values(status=TraceImportStatus.STOPPING.value)
|
|
181
|
+
)
|
|
182
|
+
await session.commit()
|
|
183
|
+
return await self._one(session, agent_id, import_id)
|
|
184
|
+
|
|
185
|
+
async def restart(self, agent_id: int, import_id: int, holder: str) -> TraceImportView:
|
|
186
|
+
async with self._sessions() as session:
|
|
187
|
+
current = await self._one(session, agent_id, import_id)
|
|
188
|
+
if current.status not in RESTARTABLE:
|
|
189
|
+
raise ImportNotActive(
|
|
190
|
+
import_id, current.status, "a failed or stopped import can be continued"
|
|
191
|
+
)
|
|
192
|
+
await _refuse_second(session, agent_id)
|
|
193
|
+
await session.execute(
|
|
194
|
+
update(models.TraceImport)
|
|
195
|
+
.where(models.TraceImport.id == import_id)
|
|
196
|
+
.values(
|
|
197
|
+
status=TraceImportStatus.RUNNING.value,
|
|
198
|
+
error=None,
|
|
199
|
+
finished_at=None,
|
|
200
|
+
lease_holder=holder,
|
|
201
|
+
lease_expires_at=_lease(),
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
await session.commit()
|
|
205
|
+
return await self._one(session, agent_id, import_id)
|
|
206
|
+
|
|
207
|
+
async def job(self, import_id: int) -> ImportJob:
|
|
208
|
+
async with self._sessions() as session:
|
|
209
|
+
row = await session.get(models.TraceImport, import_id)
|
|
210
|
+
if row is None:
|
|
211
|
+
raise UnknownTraceImport(import_id)
|
|
212
|
+
return ImportJob(
|
|
213
|
+
id=row.id,
|
|
214
|
+
agent_id=row.agent_id,
|
|
215
|
+
trace_source_id=row.trace_source_id,
|
|
216
|
+
project=row.project,
|
|
217
|
+
until=row.until,
|
|
218
|
+
resume_from=row.resume_from,
|
|
219
|
+
resume_trace_ids=list(row.resume_trace_ids),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
async def advance(
|
|
223
|
+
self,
|
|
224
|
+
import_id: int,
|
|
225
|
+
holder: str,
|
|
226
|
+
*,
|
|
227
|
+
resume_from: datetime,
|
|
228
|
+
resume_trace_ids: list[str],
|
|
229
|
+
traces: int,
|
|
230
|
+
spans: int,
|
|
231
|
+
sample_session: str | None,
|
|
232
|
+
) -> TraceImportStatus | None:
|
|
233
|
+
async with self._sessions() as session:
|
|
234
|
+
status = await session.scalar(
|
|
235
|
+
update(models.TraceImport)
|
|
236
|
+
.where(
|
|
237
|
+
models.TraceImport.id == import_id,
|
|
238
|
+
models.TraceImport.lease_holder == holder,
|
|
239
|
+
models.TraceImport.status.in_(ACTIVE),
|
|
240
|
+
)
|
|
241
|
+
.values(
|
|
242
|
+
resume_from=resume_from,
|
|
243
|
+
resume_trace_ids=resume_trace_ids,
|
|
244
|
+
traces=models.TraceImport.traces + traces,
|
|
245
|
+
spans=models.TraceImport.spans + spans,
|
|
246
|
+
lease_expires_at=_lease(),
|
|
247
|
+
**({"sample_session": sample_session} if sample_session else {}),
|
|
248
|
+
)
|
|
249
|
+
.returning(models.TraceImport.status)
|
|
250
|
+
)
|
|
251
|
+
await session.commit()
|
|
252
|
+
return TraceImportStatus(status) if status is not None else None
|
|
253
|
+
|
|
254
|
+
async def renew(self, import_id: int, holder: str) -> TraceImportStatus | None:
|
|
255
|
+
async with self._sessions() as session:
|
|
256
|
+
status = await session.scalar(
|
|
257
|
+
update(models.TraceImport)
|
|
258
|
+
.where(
|
|
259
|
+
models.TraceImport.id == import_id,
|
|
260
|
+
models.TraceImport.lease_holder == holder,
|
|
261
|
+
models.TraceImport.status.in_(ACTIVE),
|
|
262
|
+
)
|
|
263
|
+
.values(lease_expires_at=_lease())
|
|
264
|
+
.returning(models.TraceImport.status)
|
|
265
|
+
)
|
|
266
|
+
await session.commit()
|
|
267
|
+
return TraceImportStatus(status) if status is not None else None
|
|
268
|
+
|
|
269
|
+
async def finish(
|
|
270
|
+
self, import_id: int, holder: str, status: TraceImportStatus, error: str | None = None
|
|
271
|
+
) -> None:
|
|
272
|
+
async with self._sessions() as session:
|
|
273
|
+
await session.execute(
|
|
274
|
+
update(models.TraceImport)
|
|
275
|
+
.where(
|
|
276
|
+
models.TraceImport.id == import_id, models.TraceImport.lease_holder == holder
|
|
277
|
+
)
|
|
278
|
+
.values(
|
|
279
|
+
status=status.value,
|
|
280
|
+
error=error,
|
|
281
|
+
finished_at=func.now(),
|
|
282
|
+
lease_holder=None,
|
|
283
|
+
lease_expires_at=None,
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
await session.commit()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def reap_imports(session_factory: async_sessionmaker[AsyncSession]) -> list[int]:
|
|
290
|
+
async with session_factory() as session:
|
|
291
|
+
reaped = await session.scalars(
|
|
292
|
+
update(models.TraceImport)
|
|
293
|
+
.where(
|
|
294
|
+
models.TraceImport.status.in_(ACTIVE),
|
|
295
|
+
models.TraceImport.lease_expires_at < datetime.now(UTC),
|
|
296
|
+
)
|
|
297
|
+
.values(
|
|
298
|
+
status=TraceImportStatus.FAILED.value,
|
|
299
|
+
error=IMPORT_STOPPED_ERROR,
|
|
300
|
+
finished_at=func.now(),
|
|
301
|
+
lease_holder=None,
|
|
302
|
+
lease_expires_at=None,
|
|
303
|
+
)
|
|
304
|
+
.returning(models.TraceImport.id)
|
|
305
|
+
)
|
|
306
|
+
ids = list(reaped.all())
|
|
307
|
+
await session.commit()
|
|
308
|
+
return ids
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, SecretStr
|
|
4
|
+
from sqlalchemy import select
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
6
|
+
|
|
7
|
+
from evalkit.contracts.enums import TraceProvider
|
|
8
|
+
from evalkit.db import models
|
|
9
|
+
from evalkit.db.crypto import SecretBox
|
|
10
|
+
from evalkit.db.locks import lock_name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnknownTraceSource(KeyError):
|
|
14
|
+
def __init__(self, name: str):
|
|
15
|
+
super().__init__(f"unknown trace source: {name}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TraceSourceExists(ValueError):
|
|
19
|
+
def __init__(self, name: str):
|
|
20
|
+
super().__init__(f"trace source exists: {name}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EncryptionKeyMissing(RuntimeError):
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
super().__init__("encryption_key is not configured")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TraceSourceView(BaseModel):
|
|
29
|
+
id: int
|
|
30
|
+
name: str
|
|
31
|
+
provider: TraceProvider
|
|
32
|
+
url: str
|
|
33
|
+
archived: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ResolvedTraceSource(BaseModel):
|
|
37
|
+
id: int
|
|
38
|
+
name: str
|
|
39
|
+
provider: TraceProvider
|
|
40
|
+
url: str
|
|
41
|
+
api_key: SecretStr | None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _view(row: models.TraceSource) -> TraceSourceView:
|
|
45
|
+
return TraceSourceView(
|
|
46
|
+
id=row.id,
|
|
47
|
+
name=row.name,
|
|
48
|
+
provider=TraceProvider(row.provider),
|
|
49
|
+
url=row.config["url"],
|
|
50
|
+
archived=row.archived_at is not None,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TraceSourceStore:
|
|
55
|
+
def __init__(self, session_factory: async_sessionmaker[AsyncSession], box: SecretBox | None):
|
|
56
|
+
self._sessions = session_factory
|
|
57
|
+
self._box = box
|
|
58
|
+
|
|
59
|
+
def _seal(self, api_key: str) -> bytes:
|
|
60
|
+
if self._box is None:
|
|
61
|
+
raise EncryptionKeyMissing()
|
|
62
|
+
return self._box.seal(api_key)
|
|
63
|
+
|
|
64
|
+
async def _row(self, session: AsyncSession, name: str) -> models.TraceSource:
|
|
65
|
+
row = await session.scalar(
|
|
66
|
+
select(models.TraceSource).where(models.TraceSource.name == name)
|
|
67
|
+
)
|
|
68
|
+
if row is None:
|
|
69
|
+
raise UnknownTraceSource(name)
|
|
70
|
+
return row
|
|
71
|
+
|
|
72
|
+
async def create(
|
|
73
|
+
self, name: str, provider: TraceProvider, url: str, api_key: str | None
|
|
74
|
+
) -> TraceSourceView:
|
|
75
|
+
sealed = self._seal(api_key) if api_key else None
|
|
76
|
+
async with self._sessions() as session:
|
|
77
|
+
await lock_name(session, "trace-source", name)
|
|
78
|
+
if await session.scalar(
|
|
79
|
+
select(models.TraceSource.id).where(models.TraceSource.name == name)
|
|
80
|
+
):
|
|
81
|
+
raise TraceSourceExists(name)
|
|
82
|
+
row = models.TraceSource(
|
|
83
|
+
name=name, provider=provider.value, config={"url": url}, api_key_encrypted=sealed
|
|
84
|
+
)
|
|
85
|
+
session.add(row)
|
|
86
|
+
await session.commit()
|
|
87
|
+
return _view(row)
|
|
88
|
+
|
|
89
|
+
async def edit(
|
|
90
|
+
self, name: str, *, url: str | None = None, api_key: str | None = None
|
|
91
|
+
) -> TraceSourceView:
|
|
92
|
+
async with self._sessions() as session:
|
|
93
|
+
row = await self._row(session, name)
|
|
94
|
+
if url is not None:
|
|
95
|
+
row.config = {**row.config, "url": url}
|
|
96
|
+
if api_key is not None:
|
|
97
|
+
row.api_key_encrypted = self._seal(api_key)
|
|
98
|
+
await session.commit()
|
|
99
|
+
return _view(row)
|
|
100
|
+
|
|
101
|
+
async def archive(self, name: str) -> TraceSourceView:
|
|
102
|
+
async with self._sessions() as session:
|
|
103
|
+
row = await self._row(session, name)
|
|
104
|
+
row.archived_at = datetime.now(UTC)
|
|
105
|
+
await session.commit()
|
|
106
|
+
return _view(row)
|
|
107
|
+
|
|
108
|
+
async def list_sources(self, include_archived: bool = False) -> list[TraceSourceView]:
|
|
109
|
+
query = select(models.TraceSource).order_by(models.TraceSource.name)
|
|
110
|
+
if not include_archived:
|
|
111
|
+
query = query.where(models.TraceSource.archived_at.is_(None))
|
|
112
|
+
async with self._sessions() as session:
|
|
113
|
+
return [_view(row) for row in (await session.scalars(query)).all()]
|
|
114
|
+
|
|
115
|
+
async def resolve(self, name: str) -> ResolvedTraceSource:
|
|
116
|
+
async with self._sessions() as session:
|
|
117
|
+
row = await self._row(session, name)
|
|
118
|
+
if row.archived_at is not None:
|
|
119
|
+
raise UnknownTraceSource(name)
|
|
120
|
+
api_key = None
|
|
121
|
+
if row.api_key_encrypted is not None:
|
|
122
|
+
if self._box is None:
|
|
123
|
+
raise EncryptionKeyMissing()
|
|
124
|
+
api_key = self._box.open(row.api_key_encrypted)
|
|
125
|
+
return ResolvedTraceSource(
|
|
126
|
+
id=row.id,
|
|
127
|
+
name=row.name,
|
|
128
|
+
provider=TraceProvider(row.provider),
|
|
129
|
+
url=row.config["url"],
|
|
130
|
+
api_key=SecretStr(api_key) if api_key is not None else None,
|
|
131
|
+
)
|
evalkit/db/users.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
|
+
|
|
4
|
+
import bcrypt
|
|
5
|
+
from sqlalchemy import delete, func, select, union_all, update
|
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
7
|
+
|
|
8
|
+
from evalkit.contracts.enums import TokenKind
|
|
9
|
+
from evalkit.db.locks import lock_name
|
|
10
|
+
from evalkit.db.models import AuthToken, User, Workspace
|
|
11
|
+
|
|
12
|
+
NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NameTaken(Exception):
|
|
16
|
+
def __init__(self, name: str):
|
|
17
|
+
super().__init__(f"name {name!r} is already a user or workspace")
|
|
18
|
+
self.name = name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InvalidName(ValueError):
|
|
22
|
+
def __init__(self, name: str):
|
|
23
|
+
super().__init__(
|
|
24
|
+
f"invalid name {name!r}: use lowercase letters, digits, '.', '_' or '-', "
|
|
25
|
+
"starting with a letter or digit"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_name(name: str) -> None:
|
|
30
|
+
if not NAME_PATTERN.fullmatch(name):
|
|
31
|
+
raise InvalidName(name)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def lock_names(session: AsyncSession) -> None:
|
|
35
|
+
await lock_name(session, "users-and-workspaces")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LastAdmin(Exception):
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
super().__init__("cannot remove the last admin")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UnknownUser(KeyError):
|
|
44
|
+
def __init__(self, username: str):
|
|
45
|
+
super().__init__(f"unknown user: {username}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class InvalidPassword(ValueError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
BCRYPT_ROUNDS = 12
|
|
53
|
+
PASSWORD_MIN_CHARS = 8
|
|
54
|
+
PASSWORD_MAX_BYTES = 72
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_password_size(password: str) -> None:
|
|
58
|
+
if len(password.encode()) > PASSWORD_MAX_BYTES:
|
|
59
|
+
raise InvalidPassword(f"password is longer than {PASSWORD_MAX_BYTES} bytes")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_new_password(password: str) -> None:
|
|
63
|
+
if len(password) < PASSWORD_MIN_CHARS:
|
|
64
|
+
raise InvalidPassword(f"password must be at least {PASSWORD_MIN_CHARS} characters")
|
|
65
|
+
check_password_size(password)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def hash_password(password: str) -> str:
|
|
69
|
+
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(BCRYPT_ROUNDS)).decode()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def verify_password(password: str, password_hash: str) -> bool:
|
|
73
|
+
return bcrypt.checkpw(password.encode(), password_hash.encode())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def name_taken(session: AsyncSession, name: str) -> bool:
|
|
77
|
+
count = await session.scalar(
|
|
78
|
+
select(func.count()).select_from(
|
|
79
|
+
union_all(
|
|
80
|
+
select(User.id).where(User.username == name),
|
|
81
|
+
select(Workspace.id).where(Workspace.name == name),
|
|
82
|
+
).subquery()
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
return bool(count)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def user_id(session: AsyncSession, username: str) -> int:
|
|
89
|
+
found = await session.scalar(select(User.id).where(User.username == username))
|
|
90
|
+
if found is None:
|
|
91
|
+
raise UnknownUser(username)
|
|
92
|
+
return found
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class UserStore:
|
|
96
|
+
def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
|
|
97
|
+
self._sf = session_factory
|
|
98
|
+
|
|
99
|
+
async def create(self, username: str, password: str, *, is_admin: bool = False) -> User:
|
|
100
|
+
check_name(username)
|
|
101
|
+
check_new_password(password)
|
|
102
|
+
async with self._sf() as s:
|
|
103
|
+
await lock_names(s)
|
|
104
|
+
if await name_taken(s, username):
|
|
105
|
+
raise NameTaken(username)
|
|
106
|
+
user = User(username=username, password_hash=hash_password(password), is_admin=is_admin)
|
|
107
|
+
s.add(user)
|
|
108
|
+
await s.flush()
|
|
109
|
+
s.add(Workspace(name=username, owner_user_id=user.id))
|
|
110
|
+
await s.commit()
|
|
111
|
+
return user
|
|
112
|
+
|
|
113
|
+
async def _get(self, s: AsyncSession, username: str) -> User:
|
|
114
|
+
user = await s.scalar(select(User).where(User.username == username))
|
|
115
|
+
if user is None:
|
|
116
|
+
raise UnknownUser(username)
|
|
117
|
+
return user
|
|
118
|
+
|
|
119
|
+
async def _ensure_other_admin(self, s: AsyncSession, user: User) -> None:
|
|
120
|
+
if not user.is_admin or user.archived_at is not None:
|
|
121
|
+
return
|
|
122
|
+
await lock_name(s, "admins")
|
|
123
|
+
others = await s.scalar(
|
|
124
|
+
select(func.count()).where(
|
|
125
|
+
User.is_admin.is_(True), User.archived_at.is_(None), User.id != user.id
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
if not others:
|
|
129
|
+
raise LastAdmin()
|
|
130
|
+
|
|
131
|
+
async def archive(self, username: str) -> None:
|
|
132
|
+
now = datetime.now(UTC)
|
|
133
|
+
async with self._sf() as s:
|
|
134
|
+
user = await self._get(s, username)
|
|
135
|
+
await self._ensure_other_admin(s, user)
|
|
136
|
+
user.archived_at = now
|
|
137
|
+
await s.execute(
|
|
138
|
+
delete(AuthToken).where(
|
|
139
|
+
AuthToken.user_id == user.id, AuthToken.kind != TokenKind.AGENT.value
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
await s.execute(
|
|
143
|
+
update(Workspace)
|
|
144
|
+
.where(Workspace.owner_user_id == user.id, Workspace.archived_at.is_(None))
|
|
145
|
+
.values(archived_at=now)
|
|
146
|
+
)
|
|
147
|
+
await s.commit()
|
|
148
|
+
|
|
149
|
+
async def restore(self, username: str) -> None:
|
|
150
|
+
async with self._sf() as s:
|
|
151
|
+
user = await self._get(s, username)
|
|
152
|
+
if user.archived_at is not None:
|
|
153
|
+
await s.execute(
|
|
154
|
+
update(Workspace)
|
|
155
|
+
.where(
|
|
156
|
+
Workspace.owner_user_id == user.id,
|
|
157
|
+
Workspace.archived_at == user.archived_at,
|
|
158
|
+
)
|
|
159
|
+
.values(archived_at=None)
|
|
160
|
+
)
|
|
161
|
+
user.archived_at = None
|
|
162
|
+
await s.commit()
|
|
163
|
+
|
|
164
|
+
async def set_password(self, username: str, password: str) -> None:
|
|
165
|
+
check_new_password(password)
|
|
166
|
+
async with self._sf() as s:
|
|
167
|
+
user = await self._get(s, username)
|
|
168
|
+
user.password_hash = hash_password(password)
|
|
169
|
+
await s.execute(
|
|
170
|
+
delete(AuthToken).where(
|
|
171
|
+
AuthToken.user_id == user.id, AuthToken.kind != TokenKind.AGENT.value
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
await s.commit()
|
|
175
|
+
|
|
176
|
+
async def set_admin(self, username: str, value: bool) -> None:
|
|
177
|
+
async with self._sf() as s:
|
|
178
|
+
user = await self._get(s, username)
|
|
179
|
+
if not value:
|
|
180
|
+
await self._ensure_other_admin(s, user)
|
|
181
|
+
user.is_admin = value
|
|
182
|
+
await s.commit()
|
|
183
|
+
|
|
184
|
+
async def list(self) -> list[User]:
|
|
185
|
+
async with self._sf() as s:
|
|
186
|
+
return list((await s.scalars(select(User).order_by(User.username))).all())
|