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/api/meta.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request
|
|
2
|
+
|
|
3
|
+
from evalkit.api.deps import AgentStoreDep, ConfigDep
|
|
4
|
+
|
|
5
|
+
router = APIRouter(prefix="/api/meta", tags=["meta"])
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@router.get("")
|
|
9
|
+
async def meta(request: Request, cfg: ConfigDep, agents: AgentStoreDep) -> dict:
|
|
10
|
+
every = await agents.list_agents()
|
|
11
|
+
return {
|
|
12
|
+
"accent_color": cfg.server.accent_color,
|
|
13
|
+
"agents": [a.ref for a in every if a.workspace_is_shared and a.harness_url],
|
|
14
|
+
"agent_repos": {a.ref: a.repo_url for a in every if a.repo_url},
|
|
15
|
+
"settle_seconds": cfg.otel.settle_seconds,
|
|
16
|
+
"trace_sources": [
|
|
17
|
+
source.name for source in await request.app.state.trace_sources.list_sources()
|
|
18
|
+
],
|
|
19
|
+
}
|
evalkit/api/otel.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
11
|
+
from fastapi.exception_handlers import http_exception_handler
|
|
12
|
+
from google.protobuf.message import DecodeError
|
|
13
|
+
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import (
|
|
14
|
+
ExportTraceServiceRequest,
|
|
15
|
+
ExportTraceServiceResponse,
|
|
16
|
+
)
|
|
17
|
+
from opentelemetry.proto.common.v1.common_pb2 import AnyValue
|
|
18
|
+
from opentelemetry.proto.trace.v1.trace_pb2 import Span as ProtoSpan
|
|
19
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
20
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
21
|
+
from starlette.responses import Response
|
|
22
|
+
|
|
23
|
+
from evalkit.api.access import load_actor, workspace_ref
|
|
24
|
+
from evalkit.api.auth import CollectorPrincipal, Principal
|
|
25
|
+
from evalkit.api.deps import AgentStoreDep, ConfigDep, MappingStoreDep, SessionDep, SpanStoreDep
|
|
26
|
+
from evalkit.contracts.esf import MappingDocument
|
|
27
|
+
from evalkit.core.access import Actor, WorkspaceRef, can_write
|
|
28
|
+
from evalkit.core.agents import AgentDefinition, UnknownAgent, explicit
|
|
29
|
+
from evalkit.core.traces.mapping import session_id_of
|
|
30
|
+
from evalkit.core.traces.raw import RawSpan
|
|
31
|
+
from evalkit.core.workspace import Reference, parse_reference, slug
|
|
32
|
+
from evalkit.db.agents import AgentStore
|
|
33
|
+
from evalkit.db.models import User
|
|
34
|
+
|
|
35
|
+
router = APIRouter(prefix="/api/otel/v1", tags=["otel"])
|
|
36
|
+
LOGGER = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
AGENT_HEADER = "X-Evalkit-Agent"
|
|
39
|
+
AGENT_ATTRIBUTE = "evalkit.agent"
|
|
40
|
+
PROTOBUF = "application/x-protobuf"
|
|
41
|
+
SPAN_KINDS = {
|
|
42
|
+
0: "UNSPECIFIED",
|
|
43
|
+
1: "INTERNAL",
|
|
44
|
+
2: "SERVER",
|
|
45
|
+
3: "CLIENT",
|
|
46
|
+
4: "PRODUCER",
|
|
47
|
+
5: "CONSUMER",
|
|
48
|
+
}
|
|
49
|
+
STATUS_CODES = {0: "UNSET", 1: "OK", 2: "ERROR"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _any_value(value: AnyValue) -> Any:
|
|
53
|
+
kind = value.WhichOneof("value")
|
|
54
|
+
if kind is None:
|
|
55
|
+
return None
|
|
56
|
+
if kind == "array_value":
|
|
57
|
+
return [_any_value(v) for v in value.array_value.values]
|
|
58
|
+
if kind == "kvlist_value":
|
|
59
|
+
return {kv.key: _any_value(kv.value) for kv in value.kvlist_value.values}
|
|
60
|
+
if kind == "bytes_value":
|
|
61
|
+
return value.bytes_value.hex()
|
|
62
|
+
return getattr(value, kind)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _attributes(items: Any) -> dict[str, Any]:
|
|
66
|
+
return {kv.key: _any_value(kv.value) for kv in items}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _time(nanos: int) -> datetime | None:
|
|
70
|
+
return datetime.fromtimestamp(nanos / 1e9, tz=UTC) if nanos else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _iso(nanos: int) -> str | None:
|
|
74
|
+
at = _time(nanos)
|
|
75
|
+
return at.isoformat() if at else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _from_proto(span: ProtoSpan, resource: dict[str, Any]) -> RawSpan:
|
|
79
|
+
start = _time(span.start_time_unix_nano)
|
|
80
|
+
if start is None:
|
|
81
|
+
raise HTTPException(status_code=400, detail=f"span {span.name!r} has no start time")
|
|
82
|
+
return RawSpan(
|
|
83
|
+
trace_id=span.trace_id.hex(),
|
|
84
|
+
span_id=span.span_id.hex(),
|
|
85
|
+
parent_span_id=span.parent_span_id.hex() or None,
|
|
86
|
+
name=span.name,
|
|
87
|
+
kind=SPAN_KINDS.get(span.kind, "INTERNAL"),
|
|
88
|
+
start_time=start,
|
|
89
|
+
end_time=_time(span.end_time_unix_nano),
|
|
90
|
+
status_code=STATUS_CODES.get(span.status.code, "UNSET"),
|
|
91
|
+
status_message=span.status.message or None,
|
|
92
|
+
attributes=_attributes(span.attributes),
|
|
93
|
+
events=[
|
|
94
|
+
{
|
|
95
|
+
"name": e.name,
|
|
96
|
+
"time": _iso(e.time_unix_nano),
|
|
97
|
+
"attributes": _attributes(e.attributes),
|
|
98
|
+
}
|
|
99
|
+
for e in span.events
|
|
100
|
+
],
|
|
101
|
+
resource=resource,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _json_any(value: dict[str, Any]) -> Any:
|
|
106
|
+
if "stringValue" in value:
|
|
107
|
+
return value["stringValue"]
|
|
108
|
+
if "intValue" in value:
|
|
109
|
+
return int(value["intValue"])
|
|
110
|
+
if "doubleValue" in value:
|
|
111
|
+
return float(value["doubleValue"])
|
|
112
|
+
if "boolValue" in value:
|
|
113
|
+
return bool(value["boolValue"])
|
|
114
|
+
if "arrayValue" in value:
|
|
115
|
+
return [_json_any(v) for v in value["arrayValue"].get("values", [])]
|
|
116
|
+
if "kvlistValue" in value:
|
|
117
|
+
return {kv["key"]: _json_any(kv["value"]) for kv in value["kvlistValue"].get("values", [])}
|
|
118
|
+
if "bytesValue" in value:
|
|
119
|
+
return value["bytesValue"]
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _json_attributes(items: list[dict[str, Any]] | None) -> dict[str, Any]:
|
|
124
|
+
return {kv["key"]: _json_any(kv.get("value", {})) for kv in items or []}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _from_json(span: dict[str, Any], resource: dict[str, Any]) -> RawSpan:
|
|
128
|
+
start = _time(int(span.get("startTimeUnixNano") or 0))
|
|
129
|
+
if start is None:
|
|
130
|
+
raise HTTPException(status_code=400, detail="span without startTimeUnixNano")
|
|
131
|
+
status = span.get("status") or {}
|
|
132
|
+
return RawSpan(
|
|
133
|
+
trace_id=span["traceId"],
|
|
134
|
+
span_id=span["spanId"],
|
|
135
|
+
parent_span_id=span.get("parentSpanId") or None,
|
|
136
|
+
name=span.get("name", ""),
|
|
137
|
+
kind=SPAN_KINDS.get(int(span.get("kind") or 0), "INTERNAL"),
|
|
138
|
+
start_time=start,
|
|
139
|
+
end_time=_time(int(span.get("endTimeUnixNano") or 0)),
|
|
140
|
+
status_code=STATUS_CODES.get(int(status.get("code") or 0), "UNSET"),
|
|
141
|
+
status_message=status.get("message") or None,
|
|
142
|
+
attributes=_json_attributes(span.get("attributes")),
|
|
143
|
+
events=[
|
|
144
|
+
{
|
|
145
|
+
"name": e.get("name"),
|
|
146
|
+
"time": _iso(int(e.get("timeUnixNano") or 0)),
|
|
147
|
+
"attributes": _json_attributes(e.get("attributes")),
|
|
148
|
+
}
|
|
149
|
+
for e in span.get("events") or []
|
|
150
|
+
],
|
|
151
|
+
resource=resource,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _decode(body: bytes, content_type: str) -> list[RawSpan]:
|
|
156
|
+
spans: list[RawSpan] = []
|
|
157
|
+
if content_type.startswith(PROTOBUF):
|
|
158
|
+
request = ExportTraceServiceRequest.FromString(body)
|
|
159
|
+
for resource_spans in request.resource_spans:
|
|
160
|
+
resource = _attributes(resource_spans.resource.attributes)
|
|
161
|
+
for scope_spans in resource_spans.scope_spans:
|
|
162
|
+
spans.extend(_from_proto(span, resource) for span in scope_spans.spans)
|
|
163
|
+
return spans
|
|
164
|
+
if content_type.startswith("application/json"):
|
|
165
|
+
payload = json.loads(body)
|
|
166
|
+
for resource_spans in payload.get("resourceSpans", []):
|
|
167
|
+
resource = _json_attributes((resource_spans.get("resource") or {}).get("attributes"))
|
|
168
|
+
for scope_spans in resource_spans.get("scopeSpans", []):
|
|
169
|
+
spans.extend(_from_json(span, resource) for span in scope_spans.get("spans", []))
|
|
170
|
+
return spans
|
|
171
|
+
raise HTTPException(
|
|
172
|
+
status_code=415,
|
|
173
|
+
detail=f"unsupported content type {content_type!r}; send {PROTOBUF} or application/json",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def decode_otlp(body: bytes, content_type: str) -> list[RawSpan]:
|
|
178
|
+
try:
|
|
179
|
+
return _decode(body, content_type)
|
|
180
|
+
except (DecodeError, ValueError, KeyError, TypeError) as exc:
|
|
181
|
+
raise HTTPException(status_code=400, detail=f"malformed OTLP body: {exc!r}") from exc
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Rejected(HTTPException):
|
|
185
|
+
def __init__(self, status_code: int, detail: str, reason: str, agent: str | None) -> None:
|
|
186
|
+
super().__init__(status_code=status_code, detail=detail)
|
|
187
|
+
self.reason = reason
|
|
188
|
+
self.agent = agent
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _reason(exc: StarletteHTTPException) -> str:
|
|
192
|
+
if isinstance(exc, Rejected):
|
|
193
|
+
return exc.reason
|
|
194
|
+
if exc.status_code == 401:
|
|
195
|
+
return "expired token" if exc.detail == "token expired" else "invalid token"
|
|
196
|
+
if exc.status_code == 403:
|
|
197
|
+
return "forbidden"
|
|
198
|
+
if exc.status_code == 413:
|
|
199
|
+
return "too large"
|
|
200
|
+
return "malformed"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def log_rejection(request: Request, exc: Exception) -> Response:
|
|
204
|
+
assert isinstance(exc, StarletteHTTPException)
|
|
205
|
+
if request.url.path.startswith(router.prefix):
|
|
206
|
+
agent = exc.agent if isinstance(exc, Rejected) else request.headers.get(AGENT_HEADER)
|
|
207
|
+
reason = _reason(exc)
|
|
208
|
+
LOGGER.warning(
|
|
209
|
+
"otel export rejected: status=%s reason=%s agent=%s detail=%s",
|
|
210
|
+
exc.status_code,
|
|
211
|
+
reason,
|
|
212
|
+
agent,
|
|
213
|
+
exc.detail,
|
|
214
|
+
extra={
|
|
215
|
+
"otel_rejection": {
|
|
216
|
+
"status": exc.status_code,
|
|
217
|
+
"reason": reason,
|
|
218
|
+
"agent": agent,
|
|
219
|
+
"detail": exc.detail,
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
return await http_exception_handler(request, exc)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _named_agent(span: RawSpan, header: str | None) -> str | None:
|
|
227
|
+
for source in (span.attributes, span.resource):
|
|
228
|
+
value = source.get(AGENT_ATTRIBUTE)
|
|
229
|
+
if value:
|
|
230
|
+
return str(value)
|
|
231
|
+
return header
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _token_agent(named: str | None, bound: AgentDefinition) -> None:
|
|
235
|
+
ref = Reference(bound.name, bound.workspace)
|
|
236
|
+
if named is None:
|
|
237
|
+
return
|
|
238
|
+
try:
|
|
239
|
+
parsed = parse_reference(named, bound.workspace)
|
|
240
|
+
except ValueError as exc:
|
|
241
|
+
raise Rejected(400, str(exc), "malformed", named) from exc
|
|
242
|
+
if parsed != ref:
|
|
243
|
+
raise Rejected(
|
|
244
|
+
403, f"token is bound to {slug(ref)}, batch names {named}", "forbidden", named
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _user_agent(named: str | None, user: User) -> Reference:
|
|
249
|
+
if named is None:
|
|
250
|
+
raise Rejected(
|
|
251
|
+
400,
|
|
252
|
+
f"name the agent with the {AGENT_HEADER} header or the {AGENT_ATTRIBUTE} span or "
|
|
253
|
+
"resource attribute",
|
|
254
|
+
"malformed",
|
|
255
|
+
None,
|
|
256
|
+
)
|
|
257
|
+
try:
|
|
258
|
+
return parse_reference(named, user.username)
|
|
259
|
+
except ValueError as exc:
|
|
260
|
+
raise Rejected(400, str(exc), "malformed", named) from exc
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
async def _user_record(
|
|
264
|
+
ref: Reference, actor: Actor, session: AsyncSession, agents: AgentStore
|
|
265
|
+
) -> AgentDefinition:
|
|
266
|
+
agent = await agents.find(explicit(ref))
|
|
267
|
+
if agent is None:
|
|
268
|
+
unknown = UnknownAgent(explicit(ref)).args[0]
|
|
269
|
+
raise Rejected(404, unknown, "unknown agent", explicit(ref))
|
|
270
|
+
ws = await workspace_ref(session, agent.workspace)
|
|
271
|
+
if not can_write(actor, ws):
|
|
272
|
+
raise Rejected(403, f"you cannot export traces for {agent.ref}", "forbidden", agent.ref)
|
|
273
|
+
return _live(agent, ws)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _live(agent: AgentDefinition, ws: WorkspaceRef) -> AgentDefinition:
|
|
277
|
+
if ws.archived:
|
|
278
|
+
raise Rejected(409, f"workspace {ws.name} is archived", "archived", agent.ref)
|
|
279
|
+
if agent.archived:
|
|
280
|
+
raise Rejected(409, f"agent {agent.ref} is archived", "archived", agent.ref)
|
|
281
|
+
return agent
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def _resolve(
|
|
285
|
+
principal: Principal,
|
|
286
|
+
groups: dict[str | None, list[RawSpan]],
|
|
287
|
+
session: AsyncSession,
|
|
288
|
+
agents: AgentStore,
|
|
289
|
+
) -> list[tuple[AgentDefinition, list[RawSpan]]]:
|
|
290
|
+
if principal.agent_id is not None:
|
|
291
|
+
bound = await agents.get_by_id(principal.agent_id)
|
|
292
|
+
for named in groups:
|
|
293
|
+
_token_agent(named, bound)
|
|
294
|
+
_live(bound, await workspace_ref(session, bound.workspace))
|
|
295
|
+
return [(bound, [span for spans in groups.values() for span in spans])]
|
|
296
|
+
assert principal.user is not None
|
|
297
|
+
user = principal.user
|
|
298
|
+
actor = await load_actor(session, user)
|
|
299
|
+
refs = {named: _user_agent(named, user) for named in groups}
|
|
300
|
+
by_ref: dict[Reference, list[RawSpan]] = {}
|
|
301
|
+
for named, spans in groups.items():
|
|
302
|
+
by_ref.setdefault(refs[named], []).extend(spans)
|
|
303
|
+
return [
|
|
304
|
+
(await _user_record(ref, actor, session, agents), spans) for ref, spans in by_ref.items()
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@router.post("/traces")
|
|
309
|
+
async def export_traces(
|
|
310
|
+
request: Request,
|
|
311
|
+
principal: CollectorPrincipal,
|
|
312
|
+
cfg: ConfigDep,
|
|
313
|
+
agents: AgentStoreDep,
|
|
314
|
+
session: SessionDep,
|
|
315
|
+
spans_store: SpanStoreDep,
|
|
316
|
+
mappings: MappingStoreDep,
|
|
317
|
+
) -> Response:
|
|
318
|
+
header = request.headers.get(AGENT_HEADER)
|
|
319
|
+
body = await request.body()
|
|
320
|
+
if request.headers.get("content-encoding") == "gzip":
|
|
321
|
+
with gzip.GzipFile(fileobj=io.BytesIO(body)) as unzipped:
|
|
322
|
+
body = unzipped.read(cfg.otel.max_batch_bytes + 1)
|
|
323
|
+
if len(body) > cfg.otel.max_batch_bytes:
|
|
324
|
+
raise Rejected(
|
|
325
|
+
413,
|
|
326
|
+
f"decompressed batch exceeds otel.max_batch_bytes ({cfg.otel.max_batch_bytes})",
|
|
327
|
+
"too large",
|
|
328
|
+
header,
|
|
329
|
+
)
|
|
330
|
+
content_type = request.headers.get("content-type", "")
|
|
331
|
+
spans = decode_otlp(body, content_type)
|
|
332
|
+
groups: dict[str | None, list[RawSpan]] = {}
|
|
333
|
+
for span in spans:
|
|
334
|
+
groups.setdefault(_named_agent(span, header), []).append(span)
|
|
335
|
+
for record, agent_spans in await _resolve(principal, groups, session, agents):
|
|
336
|
+
active = await mappings.active(record.id)
|
|
337
|
+
document = MappingDocument.model_validate(active.document) if active else None
|
|
338
|
+
if document is None:
|
|
339
|
+
LOGGER.warning(
|
|
340
|
+
"agent %s has no active span mapping; its spans are stored without a session id",
|
|
341
|
+
record.ref,
|
|
342
|
+
)
|
|
343
|
+
for span in agent_spans:
|
|
344
|
+
span.session_id_hint = session_id_of(span.attributes, document)
|
|
345
|
+
await spans_store.upsert(record.id, agent_spans)
|
|
346
|
+
if content_type.startswith(PROTOBUF):
|
|
347
|
+
return Response(
|
|
348
|
+
content=ExportTraceServiceResponse().SerializeToString(), media_type=PROTOBUF
|
|
349
|
+
)
|
|
350
|
+
return Response(content=b"{}", media_type="application/json")
|
evalkit/api/pull.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import zipfile
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from fastapi import APIRouter, HTTPException
|
|
9
|
+
from pydantic import BaseModel, Field, model_validator
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
from starlette.responses import Response
|
|
12
|
+
|
|
13
|
+
from evalkit.api.deps import (
|
|
14
|
+
AgentStoreDep,
|
|
15
|
+
RunnerDepsDep,
|
|
16
|
+
SessionDep,
|
|
17
|
+
SessionReaderDep,
|
|
18
|
+
SpanStoreDep,
|
|
19
|
+
)
|
|
20
|
+
from evalkit.api.download import content_disposition
|
|
21
|
+
from evalkit.api.runs import BrowserRun, agent_or_404, harness_client, resolve_runs
|
|
22
|
+
from evalkit.contracts.atif import TrajectoryValidationError, validate_trajectory
|
|
23
|
+
from evalkit.core.agents import AgentDefinition, server_may_drive
|
|
24
|
+
from evalkit.core.costmap import load_cost_map
|
|
25
|
+
from evalkit.core.harness import HarnessClient
|
|
26
|
+
from evalkit.core.paths import UnsafePathError
|
|
27
|
+
from evalkit.core.pull import (
|
|
28
|
+
MAX_PULL,
|
|
29
|
+
RunBundle,
|
|
30
|
+
bundle_files,
|
|
31
|
+
index_row,
|
|
32
|
+
run_body,
|
|
33
|
+
)
|
|
34
|
+
from evalkit.core.runner import RunnerDeps, stored_trajectory_doc
|
|
35
|
+
from evalkit.core.trajectory import backfill_step_costs
|
|
36
|
+
from evalkit.db.runs import (
|
|
37
|
+
RunSource,
|
|
38
|
+
agent_run,
|
|
39
|
+
eval_context,
|
|
40
|
+
experiment_run_ids,
|
|
41
|
+
)
|
|
42
|
+
from evalkit.db.sessions import DbSessionReader
|
|
43
|
+
from evalkit.store.base import AgentRunRecord
|
|
44
|
+
|
|
45
|
+
router = APIRouter(prefix="/api/agents", tags=["agents"])
|
|
46
|
+
|
|
47
|
+
UNREACHABLE = "only the CLI that runs this agent can reach its harness"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PullRequest(BaseModel):
|
|
51
|
+
source: RunSource = "all"
|
|
52
|
+
since: datetime | None = None
|
|
53
|
+
until: datetime | None = None
|
|
54
|
+
limit: int = Field(default=MAX_PULL, ge=1, le=MAX_PULL)
|
|
55
|
+
offset: int = Field(default=0, ge=0)
|
|
56
|
+
run_ids: list[str] | None = Field(default=None, min_length=1, max_length=MAX_PULL)
|
|
57
|
+
experiment_id: int | None = None
|
|
58
|
+
|
|
59
|
+
@model_validator(mode="after")
|
|
60
|
+
def _one_selection(self) -> "PullRequest":
|
|
61
|
+
if self.run_ids is not None and self.experiment_id is not None:
|
|
62
|
+
raise ValueError("pass run_ids or experiment_id, not both")
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class PullContext:
|
|
68
|
+
agent: AgentDefinition
|
|
69
|
+
deps: RunnerDeps
|
|
70
|
+
sessions: DbSessionReader
|
|
71
|
+
harness: HarnessClient | None
|
|
72
|
+
cost_map: dict
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _trajectory(
|
|
76
|
+
ctx: PullContext, run: BrowserRun, stored: AgentRunRecord | None
|
|
77
|
+
) -> dict | None:
|
|
78
|
+
if stored is not None:
|
|
79
|
+
doc, _ = await stored_trajectory_doc(ctx.deps.blob_store, ctx.agent.id, stored)
|
|
80
|
+
if doc is not None:
|
|
81
|
+
return doc
|
|
82
|
+
if ctx.agent.is_otel:
|
|
83
|
+
result = await ctx.sessions.read(ctx.agent.ref, run.run_id)
|
|
84
|
+
if result is None or result.trajectory is None:
|
|
85
|
+
raise LookupError("no spans for this session")
|
|
86
|
+
return result.trajectory
|
|
87
|
+
if ctx.harness is None:
|
|
88
|
+
raise LookupError(UNREACHABLE)
|
|
89
|
+
return (await ctx.harness.trajectory(run.run_id)).model_dump(exclude_none=True)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def _bundle(ctx: PullContext, session: AsyncSession, run: BrowserRun) -> RunBundle:
|
|
93
|
+
stored = await agent_run(session, ctx.agent.id, run.run_id)
|
|
94
|
+
context = await eval_context(session, ctx.agent.id, run.run_id)
|
|
95
|
+
await session.commit()
|
|
96
|
+
bundle = RunBundle(
|
|
97
|
+
run_id=run.run_id,
|
|
98
|
+
source=run.source,
|
|
99
|
+
status=run.status,
|
|
100
|
+
created_at=run.created_at,
|
|
101
|
+
title=run.title,
|
|
102
|
+
run=run_body(run.run_id, run.source, stored, context),
|
|
103
|
+
eval=context,
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
doc = await _trajectory(ctx, run, stored)
|
|
107
|
+
if doc is not None:
|
|
108
|
+
trajectory = validate_trajectory(doc)
|
|
109
|
+
backfill_step_costs(trajectory, ctx.cost_map)
|
|
110
|
+
bundle.trajectory = trajectory.model_dump(exclude_none=True)
|
|
111
|
+
except (httpx.HTTPError, TrajectoryValidationError, LookupError, UnsafePathError) as exc:
|
|
112
|
+
bundle.pull_error = str(exc)
|
|
113
|
+
return bundle
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def _select(
|
|
117
|
+
record: AgentDefinition, body: PullRequest, deps, spans, session
|
|
118
|
+
) -> tuple[list[BrowserRun], int, str | None]:
|
|
119
|
+
if body.run_ids is not None:
|
|
120
|
+
page = await resolve_runs(record, deps, spans, session, run_ids=body.run_ids)
|
|
121
|
+
known = {run.run_id: run for run in page.runs}
|
|
122
|
+
runs = [known.get(run_id) or BrowserRun(run_id=run_id) for run_id in body.run_ids]
|
|
123
|
+
return runs, len(runs), None
|
|
124
|
+
if body.experiment_id is not None:
|
|
125
|
+
agent_id, ids, total = await experiment_run_ids(
|
|
126
|
+
session, body.experiment_id, limit=body.limit, offset=body.offset
|
|
127
|
+
)
|
|
128
|
+
if agent_id != record.id:
|
|
129
|
+
raise HTTPException(
|
|
130
|
+
status_code=404,
|
|
131
|
+
detail=f"experiment {body.experiment_id} did not run agent {record.ref}",
|
|
132
|
+
)
|
|
133
|
+
if not ids:
|
|
134
|
+
return [], total, None
|
|
135
|
+
picked, _, _ = await _select(record, PullRequest(run_ids=ids), deps, spans, session)
|
|
136
|
+
return picked, total, None
|
|
137
|
+
page = await resolve_runs(
|
|
138
|
+
record,
|
|
139
|
+
deps,
|
|
140
|
+
spans,
|
|
141
|
+
session,
|
|
142
|
+
source=body.source,
|
|
143
|
+
since=body.since,
|
|
144
|
+
until=body.until,
|
|
145
|
+
limit=body.limit,
|
|
146
|
+
offset=body.offset,
|
|
147
|
+
)
|
|
148
|
+
return page.runs, page.total, page.harness_note
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@router.post("/{agent}/runs/pull")
|
|
152
|
+
async def pull_runs(
|
|
153
|
+
agent: str,
|
|
154
|
+
body: PullRequest,
|
|
155
|
+
deps: RunnerDepsDep,
|
|
156
|
+
agents: AgentStoreDep,
|
|
157
|
+
spans: SpanStoreDep,
|
|
158
|
+
sessions: SessionReaderDep,
|
|
159
|
+
session: SessionDep,
|
|
160
|
+
) -> Response:
|
|
161
|
+
record = await agent_or_404(agents, agent)
|
|
162
|
+
selected, total, note = await _select(record, body, deps, spans, session)
|
|
163
|
+
await session.commit()
|
|
164
|
+
harness = await harness_client(deps, record) if server_may_drive(record) and selected else None
|
|
165
|
+
ctx = PullContext(
|
|
166
|
+
agent=record,
|
|
167
|
+
deps=deps,
|
|
168
|
+
sessions=sessions,
|
|
169
|
+
harness=harness,
|
|
170
|
+
cost_map=load_cost_map(deps.cfg.cost_map),
|
|
171
|
+
)
|
|
172
|
+
bundles = []
|
|
173
|
+
try:
|
|
174
|
+
for run in selected:
|
|
175
|
+
bundles.append(await _bundle(ctx, session, run))
|
|
176
|
+
finally:
|
|
177
|
+
if ctx.harness is not None:
|
|
178
|
+
await ctx.harness.aclose()
|
|
179
|
+
index = {
|
|
180
|
+
"agent": record.ref,
|
|
181
|
+
"pulled_at": datetime.now(UTC).isoformat(),
|
|
182
|
+
"filters": body.model_dump(mode="json"),
|
|
183
|
+
"total_matching": total,
|
|
184
|
+
"note": note,
|
|
185
|
+
"runs": [index_row(bundle) for bundle in bundles],
|
|
186
|
+
}
|
|
187
|
+
buffer = io.BytesIO()
|
|
188
|
+
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
189
|
+
archive.writestr("index.json", json.dumps(index, indent=2))
|
|
190
|
+
for bundle in bundles:
|
|
191
|
+
for path, data in bundle_files(bundle).items():
|
|
192
|
+
archive.writestr(path, data)
|
|
193
|
+
return Response(
|
|
194
|
+
content=buffer.getvalue(),
|
|
195
|
+
media_type="application/zip",
|
|
196
|
+
headers=content_disposition(f"{record.ref}-runs.zip"),
|
|
197
|
+
)
|