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,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from evalkit.core.review import CaseReview, review_summary
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"MissingTargetDataset",
|
|
11
|
+
"OverlayCopy",
|
|
12
|
+
"PromotionPlan",
|
|
13
|
+
"PromotionTarget",
|
|
14
|
+
"TwinState",
|
|
15
|
+
"twin_state",
|
|
16
|
+
"twin_summary",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MissingTargetDataset(Exception):
|
|
21
|
+
def __init__(self, ref: str):
|
|
22
|
+
self.ref = ref
|
|
23
|
+
super().__init__(
|
|
24
|
+
f"{ref} does not exist; create it before approving (evalkit datasets create)"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PromotionTarget(Exception):
|
|
29
|
+
def __init__(self, workspace: str, source: str, hint: str):
|
|
30
|
+
self.workspace = workspace
|
|
31
|
+
self.source = source
|
|
32
|
+
super().__init__(f"{workspace} is the promotion target of {source}; {hint}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class OverlayCopy:
|
|
37
|
+
source_dataset: str
|
|
38
|
+
agents: tuple[str, ...]
|
|
39
|
+
tags: tuple[str, ...]
|
|
40
|
+
evaluators: dict[str, Any]
|
|
41
|
+
source_file: str | None
|
|
42
|
+
|
|
43
|
+
def key(self) -> tuple[str, tuple[str, ...], tuple[str, ...], str]:
|
|
44
|
+
return (self.source_dataset, self.agents, self.tags, repr(sorted(self.evaluators.items())))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class PromotionPlan:
|
|
49
|
+
source_ref: str
|
|
50
|
+
target_ref: str
|
|
51
|
+
target_workspace_id: int
|
|
52
|
+
target_dataset_id: int
|
|
53
|
+
case_ref: int
|
|
54
|
+
case_id: str
|
|
55
|
+
content_hash: str
|
|
56
|
+
path_tags: list[str]
|
|
57
|
+
source: str | None
|
|
58
|
+
overlays: list[OverlayCopy]
|
|
59
|
+
overlay_targets: dict[str, int]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TwinState(StrEnum):
|
|
63
|
+
ABSENT = "absent"
|
|
64
|
+
SAME = "same"
|
|
65
|
+
DIFFERS = "differs"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def twin_state(mine: str, other: str | None) -> TwinState:
|
|
69
|
+
if other is None:
|
|
70
|
+
return TwinState.ABSENT
|
|
71
|
+
return TwinState.SAME if mine == other else TwinState.DIFFERS
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def twin_summary(
|
|
75
|
+
*,
|
|
76
|
+
workspace: str,
|
|
77
|
+
dataset: str,
|
|
78
|
+
case_id: str,
|
|
79
|
+
mine: str,
|
|
80
|
+
other: str | None,
|
|
81
|
+
review: CaseReview | None,
|
|
82
|
+
my_overlays: list[OverlayCopy],
|
|
83
|
+
other_overlays: list[OverlayCopy],
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
state = twin_state(mine, other)
|
|
86
|
+
return {
|
|
87
|
+
"workspace": workspace,
|
|
88
|
+
"state": state.value,
|
|
89
|
+
"content_hash": other,
|
|
90
|
+
"review": review_summary(review) if review is not None else None,
|
|
91
|
+
"overlays_differ": state is not TwinState.ABSENT
|
|
92
|
+
and {o.key() for o in my_overlays} != {o.key() for o in other_overlays},
|
|
93
|
+
"href": f"/datasets/{dataset}@{workspace}/cases/{case_id}",
|
|
94
|
+
}
|
evalkit/core/pull.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from evalkit.contracts.enums import EvaluatorKind
|
|
12
|
+
from evalkit.contracts.harness import RunListItem, RunListParams
|
|
13
|
+
from evalkit.core.harness import HarnessClient
|
|
14
|
+
from evalkit.store.base import AgentRunRecord, EvaluationRow, RunEvalContext
|
|
15
|
+
|
|
16
|
+
UNSAFE = re.compile(r"[^A-Za-z0-9._-]")
|
|
17
|
+
MAX_PULL = 100
|
|
18
|
+
HARNESS_PAGE = 200
|
|
19
|
+
MAX_HARNESS_SCAN = 10000
|
|
20
|
+
|
|
21
|
+
Exclude = Callable[[list[str]], Awaitable[set[str]]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HarnessRuns(BaseModel):
|
|
25
|
+
runs: list[RunListItem]
|
|
26
|
+
total: int
|
|
27
|
+
capped: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
SCAN_NOTE = (
|
|
31
|
+
f"stopped after scanning {MAX_HARNESS_SCAN} harness runs; narrow the window with since/until"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RunBundle(BaseModel):
|
|
36
|
+
run_id: str
|
|
37
|
+
source: Literal["app", "eval"]
|
|
38
|
+
status: str | None = None
|
|
39
|
+
created_at: datetime | None = None
|
|
40
|
+
title: str | None = None
|
|
41
|
+
run: dict
|
|
42
|
+
trajectory: dict | None = None
|
|
43
|
+
eval: RunEvalContext | None = None
|
|
44
|
+
pull_error: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def in_window(run: RunListItem, since: datetime | None, until: datetime | None) -> bool:
|
|
48
|
+
if run.created_at is None:
|
|
49
|
+
return True
|
|
50
|
+
return (since is None or run.created_at >= since) and (until is None or run.created_at < until)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def list_harness_runs(
|
|
54
|
+
harness: HarnessClient,
|
|
55
|
+
*,
|
|
56
|
+
wanted: int,
|
|
57
|
+
since: datetime | None = None,
|
|
58
|
+
until: datetime | None = None,
|
|
59
|
+
query: str | None = None,
|
|
60
|
+
user_id: str | None = None,
|
|
61
|
+
client_id: str | None = None,
|
|
62
|
+
exclude: Exclude | None = None,
|
|
63
|
+
) -> HarnessRuns:
|
|
64
|
+
kept: list[RunListItem] = []
|
|
65
|
+
sessions: set[str] = set()
|
|
66
|
+
dropped = offset = 0
|
|
67
|
+
size = HARNESS_PAGE
|
|
68
|
+
|
|
69
|
+
def new_session(run: RunListItem) -> bool:
|
|
70
|
+
if run.session_id is None:
|
|
71
|
+
return True
|
|
72
|
+
if run.session_id in sessions:
|
|
73
|
+
return False
|
|
74
|
+
sessions.add(run.session_id)
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
while True:
|
|
78
|
+
page = await harness.list_runs(
|
|
79
|
+
RunListParams(
|
|
80
|
+
query=query,
|
|
81
|
+
user_id=user_id,
|
|
82
|
+
client_id=client_id,
|
|
83
|
+
limit=size,
|
|
84
|
+
offset=offset,
|
|
85
|
+
since=since,
|
|
86
|
+
until=until,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
window = [run for run in page.runs if in_window(run, since, until)]
|
|
90
|
+
excluded = await exclude([run.run_id for run in window]) if exclude and window else set()
|
|
91
|
+
fresh = [run for run in window if run.run_id not in excluded and new_session(run)]
|
|
92
|
+
kept += fresh
|
|
93
|
+
dropped += len(page.runs) - len(fresh)
|
|
94
|
+
offset += len(page.runs)
|
|
95
|
+
oldest = page.runs[-1].created_at if page.runs else None
|
|
96
|
+
# A short page is the end only when the harness says so; a harness that
|
|
97
|
+
# caps its page below `size` keeps being paged at its own size.
|
|
98
|
+
size = min(size, len(page.runs)) or size
|
|
99
|
+
if (
|
|
100
|
+
not page.runs
|
|
101
|
+
or offset >= page.total
|
|
102
|
+
or (since is not None and oldest is not None and oldest < since)
|
|
103
|
+
):
|
|
104
|
+
return HarnessRuns(runs=kept, total=len(kept))
|
|
105
|
+
if len(kept) >= wanted:
|
|
106
|
+
return HarnessRuns(runs=kept, total=max(page.total - dropped, len(kept)))
|
|
107
|
+
if offset >= MAX_HARNESS_SCAN:
|
|
108
|
+
return HarnessRuns(runs=kept, total=max(page.total - dropped, len(kept)), capped=True)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def safe_dir(run_id: str) -> str:
|
|
112
|
+
return UNSAFE.sub("_", run_id)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def run_body(
|
|
116
|
+
run_id: str,
|
|
117
|
+
source: Literal["app", "eval"],
|
|
118
|
+
record: AgentRunRecord | None,
|
|
119
|
+
eval_context: RunEvalContext | None,
|
|
120
|
+
) -> dict:
|
|
121
|
+
body: dict = {"run_id": run_id, "source": source}
|
|
122
|
+
if eval_context is not None:
|
|
123
|
+
body["input"] = eval_context.case.input
|
|
124
|
+
if record is not None:
|
|
125
|
+
body |= {
|
|
126
|
+
"status": record.status.value,
|
|
127
|
+
"answer": record.answer,
|
|
128
|
+
"output": record.output,
|
|
129
|
+
"error": record.error,
|
|
130
|
+
"started_at": record.started_at.isoformat(),
|
|
131
|
+
"ended_at": record.ended_at.isoformat() if record.ended_at else None,
|
|
132
|
+
"metrics": record.metrics,
|
|
133
|
+
}
|
|
134
|
+
return body
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def scores(evaluations: list[EvaluationRow]) -> dict[str, float]:
|
|
138
|
+
return {
|
|
139
|
+
row.name: row.value
|
|
140
|
+
for row in evaluations
|
|
141
|
+
if row.kind is EvaluatorKind.SCORE and row.error is None and row.value is not None
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def index_row(bundle: RunBundle) -> dict:
|
|
146
|
+
context = bundle.eval
|
|
147
|
+
return {
|
|
148
|
+
"run_id": bundle.run_id,
|
|
149
|
+
"dir": f"runs/{safe_dir(bundle.run_id)}",
|
|
150
|
+
"source": bundle.source,
|
|
151
|
+
"status": bundle.status,
|
|
152
|
+
"created_at": bundle.created_at.isoformat() if bundle.created_at else None,
|
|
153
|
+
"title": bundle.title,
|
|
154
|
+
"experiment_id": context.experiment_id if context else None,
|
|
155
|
+
"case_id": context.case.case_id if context else None,
|
|
156
|
+
"repetition": context.repetition if context else None,
|
|
157
|
+
"scores": scores(context.evaluations) if context else None,
|
|
158
|
+
"error": bundle.run.get("error"),
|
|
159
|
+
"pull_error": bundle.pull_error,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _json(doc: object) -> bytes:
|
|
164
|
+
return json.dumps(doc, indent=2, ensure_ascii=False, default=str).encode()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def bundle_files(bundle: RunBundle) -> dict[str, bytes]:
|
|
168
|
+
base = f"runs/{safe_dir(bundle.run_id)}"
|
|
169
|
+
run = (
|
|
170
|
+
bundle.run if bundle.pull_error is None else {**bundle.run, "pull_error": bundle.pull_error}
|
|
171
|
+
)
|
|
172
|
+
files = {f"{base}/run.json": _json(run)}
|
|
173
|
+
if bundle.trajectory is not None:
|
|
174
|
+
files[f"{base}/trajectory.json"] = _json(bundle.trajectory)
|
|
175
|
+
if bundle.eval is not None:
|
|
176
|
+
case = bundle.eval.case
|
|
177
|
+
files[f"{base}/case.json"] = _json(
|
|
178
|
+
{
|
|
179
|
+
"case_id": case.case_id,
|
|
180
|
+
"content_hash": case.content_hash,
|
|
181
|
+
"input": case.input,
|
|
182
|
+
"expected": case.expected,
|
|
183
|
+
"evaluators": case.effective_evaluators,
|
|
184
|
+
"tags": case.tags,
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
files[f"{base}/evaluations.json"] = _json(
|
|
188
|
+
[row.model_dump(mode="json", exclude_none=True) for row in bundle.eval.evaluations]
|
|
189
|
+
)
|
|
190
|
+
return files
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def merge_index(existing: dict | None, fresh: dict) -> dict:
|
|
194
|
+
if existing is None:
|
|
195
|
+
return fresh
|
|
196
|
+
replaced = {row["run_id"] for row in fresh["runs"]}
|
|
197
|
+
kept = [row for row in existing["runs"] if row["run_id"] not in replaced]
|
|
198
|
+
return {**fresh, "runs": kept + fresh["runs"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from evalkit.contracts.custom_evaluator import EvalCase, EvalRun, ScoreItem
|
|
11
|
+
from evalkit.core.custom_eval import EvalError
|
|
12
|
+
|
|
13
|
+
LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PythonEvaluator:
|
|
18
|
+
name: str
|
|
19
|
+
kind: Literal["evaluator", "gauge"]
|
|
20
|
+
fn: Callable[[EvalCase, EvalRun], Any]
|
|
21
|
+
requires_trajectory: bool = False
|
|
22
|
+
|
|
23
|
+
def __call__(self, case: EvalCase, run: EvalRun) -> Any:
|
|
24
|
+
return self.fn(case, run)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalise(name: str, result: Any) -> list[ScoreItem]:
|
|
28
|
+
if isinstance(result, ScoreItem):
|
|
29
|
+
return [result]
|
|
30
|
+
if isinstance(result, int | float) and not isinstance(result, bool):
|
|
31
|
+
return [ScoreItem(name=name, score=float(result))]
|
|
32
|
+
if isinstance(result, list) and all(isinstance(item, ScoreItem) for item in result):
|
|
33
|
+
return result
|
|
34
|
+
raise TypeError(
|
|
35
|
+
f"evaluator {name} returned {type(result).__name__}; "
|
|
36
|
+
"expected float, ScoreItem or list[ScoreItem]"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def score_with_python_evaluator(
|
|
41
|
+
evaluator: PythonEvaluator,
|
|
42
|
+
*,
|
|
43
|
+
case: EvalCase,
|
|
44
|
+
answer: str,
|
|
45
|
+
output: dict | None,
|
|
46
|
+
trajectory: dict,
|
|
47
|
+
) -> list[ScoreItem] | EvalError:
|
|
48
|
+
run = EvalRun(answer=answer, output=output, trajectory=trajectory)
|
|
49
|
+
try:
|
|
50
|
+
if inspect.iscoroutinefunction(evaluator.fn):
|
|
51
|
+
result = await evaluator.fn(case, run)
|
|
52
|
+
else:
|
|
53
|
+
result = await asyncio.to_thread(evaluator.fn, case, run)
|
|
54
|
+
if inspect.isawaitable(result):
|
|
55
|
+
result = await result
|
|
56
|
+
return _normalise(evaluator.name, result)
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
LOGGER.warning("python evaluator %s errored: %s", evaluator.name, exc)
|
|
59
|
+
return EvalError(evaluator=evaluator.name, message=f"{type(exc).__name__}: {exc}")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Literal, Protocol
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from evalkit.contracts.enums import EvaluatorKind
|
|
6
|
+
from evalkit.contracts.settings import RemoteEvaluatorSettings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RemoteEvaluator(BaseModel):
|
|
10
|
+
name: str
|
|
11
|
+
workspace: str
|
|
12
|
+
kind: EvaluatorKind
|
|
13
|
+
settings: RemoteEvaluatorSettings
|
|
14
|
+
archived: bool = False
|
|
15
|
+
has_api_key: bool = False
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def ref(self) -> str:
|
|
19
|
+
return f"{self.name}@{self.workspace}"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def wire_kind(self) -> Literal["evaluator", "gauge"]:
|
|
23
|
+
return "evaluator" if self.kind is EvaluatorKind.SCORE else "gauge"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EvaluatorLookup(Protocol):
|
|
27
|
+
async def in_workspace(self, workspace: str) -> dict[str, RemoteEvaluator]: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UnknownEvaluator(KeyError):
|
|
31
|
+
def __init__(self, ref: str):
|
|
32
|
+
super().__init__(f"unknown evaluator: {ref} — create it with evalkit evaluators create")
|
evalkit/core/review.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Any, NamedTuple, Protocol
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from evalkit.store.base import CaseView, DatasetRow
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DECISIVE_EVENT_KINDS",
|
|
15
|
+
"OPEN_CASE_STATES",
|
|
16
|
+
"CaseComment",
|
|
17
|
+
"CaseEvent",
|
|
18
|
+
"CaseEventKind",
|
|
19
|
+
"CaseReview",
|
|
20
|
+
"CaseState",
|
|
21
|
+
"CaseVersion",
|
|
22
|
+
"NewCaseComment",
|
|
23
|
+
"NewCaseEvent",
|
|
24
|
+
"NotReviewable",
|
|
25
|
+
"ReviewLogProtocol",
|
|
26
|
+
"ensure_reviewable",
|
|
27
|
+
"is_reviewable",
|
|
28
|
+
"group_threads",
|
|
29
|
+
"review_summary",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CaseEventKind(StrEnum):
|
|
34
|
+
SYNCED = "synced"
|
|
35
|
+
REVIEW_REQUESTED = "review_requested"
|
|
36
|
+
APPROVED = "approved"
|
|
37
|
+
CHANGES_REQUESTED = "changes_requested"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CaseState(StrEnum):
|
|
41
|
+
UNREVIEWED = "unreviewed"
|
|
42
|
+
REVIEW_REQUESTED = "review_requested"
|
|
43
|
+
CHANGES_REQUESTED = "changes_requested"
|
|
44
|
+
APPROVED = "approved"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
DECISIVE_EVENT_KINDS = frozenset(
|
|
48
|
+
{
|
|
49
|
+
CaseEventKind.REVIEW_REQUESTED,
|
|
50
|
+
CaseEventKind.APPROVED,
|
|
51
|
+
CaseEventKind.CHANGES_REQUESTED,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_CASE_STATE_FOR = {
|
|
56
|
+
CaseEventKind.REVIEW_REQUESTED: CaseState.REVIEW_REQUESTED,
|
|
57
|
+
CaseEventKind.APPROVED: CaseState.APPROVED,
|
|
58
|
+
CaseEventKind.CHANGES_REQUESTED: CaseState.CHANGES_REQUESTED,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
OPEN_CASE_STATES = frozenset({CaseState.REVIEW_REQUESTED, CaseState.CHANGES_REQUESTED})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CaseVersion(NamedTuple):
|
|
65
|
+
case_id: str
|
|
66
|
+
content_hash: str
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def of(cls, case: CaseView) -> CaseVersion:
|
|
70
|
+
return cls(case.case_id, case.content_hash)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class NewCaseEvent(BaseModel):
|
|
74
|
+
dataset: str
|
|
75
|
+
case_id: str
|
|
76
|
+
content_hash: str
|
|
77
|
+
kind: CaseEventKind
|
|
78
|
+
actor: str
|
|
79
|
+
target: str | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CaseEvent(NewCaseEvent):
|
|
83
|
+
id: int
|
|
84
|
+
created_at: datetime
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class NewCaseComment(BaseModel):
|
|
88
|
+
dataset: str
|
|
89
|
+
case_id: str
|
|
90
|
+
content_hash: str
|
|
91
|
+
parent_id: int | None = None
|
|
92
|
+
author: str
|
|
93
|
+
body: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CaseComment(NewCaseComment):
|
|
97
|
+
id: int
|
|
98
|
+
resolved: bool
|
|
99
|
+
created_at: datetime
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class CaseReview:
|
|
104
|
+
dataset: str
|
|
105
|
+
case_id: str
|
|
106
|
+
content_hash: str
|
|
107
|
+
last_event: CaseEvent | None
|
|
108
|
+
requested_to: str | None
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def unreviewed(cls, dataset: str, case_id: str, content_hash: str) -> CaseReview:
|
|
112
|
+
return cls(
|
|
113
|
+
dataset=dataset,
|
|
114
|
+
case_id=case_id,
|
|
115
|
+
content_hash=content_hash,
|
|
116
|
+
last_event=None,
|
|
117
|
+
requested_to=None,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def version(self) -> CaseVersion:
|
|
122
|
+
return CaseVersion(self.case_id, self.content_hash)
|
|
123
|
+
|
|
124
|
+
def state(self) -> CaseState:
|
|
125
|
+
if self.last_event is None:
|
|
126
|
+
return CaseState.UNREVIEWED
|
|
127
|
+
return _CASE_STATE_FOR[self.last_event.kind]
|
|
128
|
+
|
|
129
|
+
def synced(self, actor: str) -> NewCaseEvent:
|
|
130
|
+
return self._event(CaseEventKind.SYNCED, actor)
|
|
131
|
+
|
|
132
|
+
def request_review(self, actor: str, target: str) -> NewCaseEvent:
|
|
133
|
+
return self._event(CaseEventKind.REVIEW_REQUESTED, actor, target)
|
|
134
|
+
|
|
135
|
+
def approve(self, actor: str) -> NewCaseEvent:
|
|
136
|
+
return self._event(CaseEventKind.APPROVED, actor)
|
|
137
|
+
|
|
138
|
+
def request_changes(self, actor: str) -> NewCaseEvent:
|
|
139
|
+
return self._event(CaseEventKind.CHANGES_REQUESTED, actor)
|
|
140
|
+
|
|
141
|
+
def carry_over(self, actor: str, new_hash: str) -> NewCaseEvent | None:
|
|
142
|
+
if self.last_event is None or self.state() not in OPEN_CASE_STATES:
|
|
143
|
+
return None
|
|
144
|
+
return NewCaseEvent(
|
|
145
|
+
dataset=self.dataset,
|
|
146
|
+
case_id=self.case_id,
|
|
147
|
+
content_hash=new_hash,
|
|
148
|
+
kind=CaseEventKind.REVIEW_REQUESTED,
|
|
149
|
+
actor=actor,
|
|
150
|
+
target=self.requested_to or self.last_event.actor,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _event(self, kind: CaseEventKind, actor: str, target: str | None = None) -> NewCaseEvent:
|
|
154
|
+
return NewCaseEvent(
|
|
155
|
+
dataset=self.dataset,
|
|
156
|
+
case_id=self.case_id,
|
|
157
|
+
content_hash=self.content_hash,
|
|
158
|
+
kind=kind,
|
|
159
|
+
actor=actor,
|
|
160
|
+
target=target,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def review_summary(review: CaseReview) -> dict[str, Any]:
|
|
165
|
+
state = review.state()
|
|
166
|
+
return {
|
|
167
|
+
"state": state.value,
|
|
168
|
+
"requested_to": review.requested_to if state is CaseState.REVIEW_REQUESTED else None,
|
|
169
|
+
"updated_at": review.last_event.created_at if review.last_event is not None else None,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ReviewLogProtocol(Protocol):
|
|
174
|
+
async def load(self, dataset: str, versions: list[CaseVersion]) -> dict[str, CaseReview]: ...
|
|
175
|
+
|
|
176
|
+
async def latest_hashes(self, dataset: str, case_ids: list[str]) -> dict[str, str]: ...
|
|
177
|
+
|
|
178
|
+
async def append(self, events: list[NewCaseEvent]) -> None: ...
|
|
179
|
+
|
|
180
|
+
async def decide(
|
|
181
|
+
self,
|
|
182
|
+
event: NewCaseEvent,
|
|
183
|
+
comment: NewCaseComment | None,
|
|
184
|
+
after: Callable[[Any, int], Awaitable[None]] | None = None,
|
|
185
|
+
) -> None: ...
|
|
186
|
+
|
|
187
|
+
async def history(self, dataset: str, case_id: str) -> list[CaseEvent]: ...
|
|
188
|
+
|
|
189
|
+
async def comments(self, dataset: str, case_id: str) -> list[CaseComment]: ...
|
|
190
|
+
|
|
191
|
+
async def add_comment(self, comment: NewCaseComment) -> CaseComment: ...
|
|
192
|
+
|
|
193
|
+
async def get_comment(self, comment_id: int) -> CaseComment | None: ...
|
|
194
|
+
|
|
195
|
+
async def set_resolved(self, comment_id: int, resolved: bool) -> CaseComment: ...
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def group_threads(comments: list[CaseComment]) -> list[dict[str, Any]]:
|
|
199
|
+
ordered = sorted(comments, key=lambda c: (c.created_at, c.id))
|
|
200
|
+
roots = [c.model_dump() | {"replies": []} for c in ordered if c.parent_id is None]
|
|
201
|
+
by_id = {root["id"]: root for root in roots}
|
|
202
|
+
for comment in ordered:
|
|
203
|
+
if comment.parent_id is not None:
|
|
204
|
+
by_id[comment.parent_id]["replies"].append(comment.model_dump())
|
|
205
|
+
return roots
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class NotReviewable(Exception):
|
|
209
|
+
def __init__(self, dataset: str):
|
|
210
|
+
self.dataset = dataset
|
|
211
|
+
super().__init__(f"{dataset} is not a suite in a shared workspace; review does not apply")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def is_reviewable(dataset: DatasetRow) -> bool:
|
|
215
|
+
return dataset.kind == "suite" and dataset.workspace_is_shared
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def ensure_reviewable(dataset: DatasetRow) -> None:
|
|
219
|
+
if not is_reviewable(dataset):
|
|
220
|
+
raise NotReviewable(dataset.ref)
|