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,255 @@
|
|
|
1
|
+
"""Aggregation of evaluation rows into the scorecard report.
|
|
2
|
+
|
|
3
|
+
A row's bucket is resolved when it is written (`bucket_for` plus the
|
|
4
|
+
exclusions below); bucket means are computed here. `answer_rubric_score` is the
|
|
5
|
+
`content` bucket's only member; every other name the rubric evaluator
|
|
6
|
+
publishes is a diagnostic on it and stays out of the bucket — recall =
|
|
7
|
+
coverage x accuracy identically, so taking all three would double-count every
|
|
8
|
+
missed rubric; the per-type rates partition the very rubrics the score
|
|
9
|
+
already counts; and citation is a separate axis whose denominator exists only
|
|
10
|
+
on cases that ask for sources.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import statistics
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
from evalkit.store.base import AggregateRows
|
|
21
|
+
|
|
22
|
+
GAUGE_PREFIX = "gauge_"
|
|
23
|
+
|
|
24
|
+
ANSWER_RUBRIC_ORDER: tuple[str, ...] = (
|
|
25
|
+
"answer_rubric_score",
|
|
26
|
+
"answer_rubric_coverage",
|
|
27
|
+
"answer_rubric_accuracy",
|
|
28
|
+
"answer_rubric_factual_retrieval",
|
|
29
|
+
"answer_rubric_derived_computation",
|
|
30
|
+
"answer_rubric_analysis_interpretation",
|
|
31
|
+
"answer_rubric_citation",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
ANSWER_RUBRIC_DIAGNOSTICS = frozenset(ANSWER_RUBRIC_ORDER) - {"answer_rubric_score"}
|
|
35
|
+
|
|
36
|
+
EXCLUDED_FROM_BUCKETS = ANSWER_RUBRIC_DIAGNOSTICS
|
|
37
|
+
|
|
38
|
+
BUCKET_ORDER: tuple[str, ...] = (
|
|
39
|
+
"content",
|
|
40
|
+
"trajectory",
|
|
41
|
+
"presentation",
|
|
42
|
+
"efficiency",
|
|
43
|
+
"custom",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
BUCKETS: dict[str, frozenset[str]] = {
|
|
47
|
+
"content": frozenset({"answer_rubric_score"}),
|
|
48
|
+
"trajectory": frozenset(
|
|
49
|
+
{"trajectory_rubric_score", "tool_trajectory_match", "required_tools_called"}
|
|
50
|
+
),
|
|
51
|
+
"presentation": frozenset({"presentation_judge"}),
|
|
52
|
+
"efficiency": frozenset({"max_response_time"}),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_BUCKET_BY_EVALUATOR: dict[str, str] = {
|
|
56
|
+
name: bucket for bucket, members in BUCKETS.items() for name in members
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# One representative evaluator per bucket the inline score blocks show: every
|
|
60
|
+
# bucket with a single member. `custom` has no fixed member and is left out.
|
|
61
|
+
BUCKET_EVALUATOR = {
|
|
62
|
+
bucket: next(iter(members)) for bucket, members in BUCKETS.items() if len(members) == 1
|
|
63
|
+
}
|
|
64
|
+
COST_GAUGE = "gauge_agent_cost_usd"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_gauge(name: str) -> bool:
|
|
68
|
+
return name.startswith(GAUGE_PREFIX)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def gauge_places(name: str) -> int:
|
|
72
|
+
"""Display precision for a gauge value — dollar gauges need decimals,
|
|
73
|
+
count/token gauges render as whole numbers."""
|
|
74
|
+
return 4 if name.endswith("_cost_usd") else 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def bucket_for(name: str, source: str = "builtin") -> str | None:
|
|
78
|
+
if source.startswith("custom:"):
|
|
79
|
+
return "custom"
|
|
80
|
+
return _BUCKET_BY_EVALUATOR.get(name)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def order_evaluator_columns(present: Iterable[str]) -> list[str]:
|
|
84
|
+
present = set(present)
|
|
85
|
+
head = [name for name in ANSWER_RUBRIC_ORDER if name in present]
|
|
86
|
+
tail = sorted(present.difference(head))
|
|
87
|
+
return head + tail
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CaseRow(BaseModel):
|
|
91
|
+
case_id: str
|
|
92
|
+
scores: dict[str, float | None] = Field(default_factory=dict)
|
|
93
|
+
explanations: dict[str, str | None] = Field(default_factory=dict)
|
|
94
|
+
gauges: dict[str, float | None] = Field(default_factory=dict)
|
|
95
|
+
errors: dict[str, str] = Field(default_factory=dict)
|
|
96
|
+
run_errors: list[str] = Field(default_factory=list)
|
|
97
|
+
repetitions: int = 1
|
|
98
|
+
overall: float | None = None
|
|
99
|
+
baseline_overall: float | None = None
|
|
100
|
+
delta: float | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class EvaluatorStat(BaseModel):
|
|
104
|
+
name: str
|
|
105
|
+
score: float | None = None
|
|
106
|
+
cases: int = 0
|
|
107
|
+
errors: int = 0
|
|
108
|
+
bucket: str | None = None
|
|
109
|
+
source: str = "builtin"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class GaugeStat(BaseModel):
|
|
113
|
+
name: str
|
|
114
|
+
mean: float | None = None
|
|
115
|
+
median: float | None = None
|
|
116
|
+
count: int = 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Report(BaseModel):
|
|
120
|
+
experiment_id: str
|
|
121
|
+
name: str
|
|
122
|
+
dataset: str
|
|
123
|
+
metadata: dict = Field(default_factory=dict)
|
|
124
|
+
cases: list[CaseRow]
|
|
125
|
+
evaluators: list[EvaluatorStat]
|
|
126
|
+
buckets: dict[str, float | None]
|
|
127
|
+
gauges: list[GaugeStat]
|
|
128
|
+
evaluator_columns: list[str]
|
|
129
|
+
baseline_id: str | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _mean(values: list[float]) -> float | None:
|
|
133
|
+
return sum(values) / len(values) if values else None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _aggregate_rows(agg: AggregateRows) -> dict[str, CaseRow]:
|
|
137
|
+
rows = {
|
|
138
|
+
case_id: CaseRow(case_id=case_id, repetitions=n)
|
|
139
|
+
for case_id, n in agg.repetitions_seen.items()
|
|
140
|
+
}
|
|
141
|
+
bucketed: dict[str, list[float]] = {}
|
|
142
|
+
for case_id, name, mean, explanation, bucket in agg.case_scores:
|
|
143
|
+
rows[case_id].scores[name] = mean
|
|
144
|
+
rows[case_id].explanations[name] = explanation
|
|
145
|
+
if bucket is not None:
|
|
146
|
+
bucketed.setdefault(case_id, []).append(mean)
|
|
147
|
+
for case_id, name, mean in agg.case_gauges:
|
|
148
|
+
rows[case_id].gauges[name] = mean
|
|
149
|
+
for case_id, name, error in agg.case_errors:
|
|
150
|
+
rows[case_id].errors[name] = error
|
|
151
|
+
for case_id, error in agg.run_errors:
|
|
152
|
+
rows[case_id].run_errors.append(error)
|
|
153
|
+
for case_id, row in rows.items():
|
|
154
|
+
row.overall = _mean(bucketed.get(case_id, []))
|
|
155
|
+
return rows
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def assemble_report(agg: AggregateRows, baseline: AggregateRows | None = None) -> Report:
|
|
159
|
+
exp = agg.experiment
|
|
160
|
+
return _report(
|
|
161
|
+
Report(
|
|
162
|
+
experiment_id=str(exp.id),
|
|
163
|
+
name=f"#{exp.number}",
|
|
164
|
+
dataset=exp.dataset,
|
|
165
|
+
metadata={
|
|
166
|
+
**exp.config.model_dump(mode="json"),
|
|
167
|
+
"agent": exp.agent,
|
|
168
|
+
"created_by": exp.created_by,
|
|
169
|
+
"status": exp.status.value,
|
|
170
|
+
},
|
|
171
|
+
cases=[],
|
|
172
|
+
evaluators=[],
|
|
173
|
+
buckets={},
|
|
174
|
+
gauges=[],
|
|
175
|
+
evaluator_columns=[],
|
|
176
|
+
),
|
|
177
|
+
_aggregate_rows(agg),
|
|
178
|
+
agg.sources,
|
|
179
|
+
{name: bucket for _, name, _, _, bucket in agg.case_scores},
|
|
180
|
+
None if baseline is None else (str(baseline.experiment.id), _aggregate_rows(baseline)),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _report(
|
|
185
|
+
header: Report,
|
|
186
|
+
rows: dict[str, CaseRow],
|
|
187
|
+
sources: dict[str, str],
|
|
188
|
+
stored_buckets: dict[str, str | None],
|
|
189
|
+
baseline: tuple[str, dict[str, CaseRow]] | None,
|
|
190
|
+
) -> Report:
|
|
191
|
+
evaluator_names = {
|
|
192
|
+
name
|
|
193
|
+
for row in rows.values()
|
|
194
|
+
for name in list(row.scores) + list(row.errors)
|
|
195
|
+
if not is_gauge(name)
|
|
196
|
+
}
|
|
197
|
+
evaluators = []
|
|
198
|
+
for name in order_evaluator_columns(evaluator_names):
|
|
199
|
+
case_values = [
|
|
200
|
+
value for row in rows.values() if (value := row.scores.get(name)) is not None
|
|
201
|
+
]
|
|
202
|
+
evaluators.append(
|
|
203
|
+
EvaluatorStat(
|
|
204
|
+
name=name,
|
|
205
|
+
score=_mean(case_values),
|
|
206
|
+
cases=len(case_values),
|
|
207
|
+
errors=sum(1 for row in rows.values() if name in row.errors),
|
|
208
|
+
bucket=stored_buckets.get(name),
|
|
209
|
+
source=sources.get(name, "builtin"),
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
buckets: dict[str, float | None] = {}
|
|
214
|
+
for bucket in BUCKET_ORDER:
|
|
215
|
+
members = [
|
|
216
|
+
stat.score
|
|
217
|
+
for stat in evaluators
|
|
218
|
+
if stat.bucket == bucket
|
|
219
|
+
and stat.score is not None
|
|
220
|
+
and stat.name not in EXCLUDED_FROM_BUCKETS
|
|
221
|
+
]
|
|
222
|
+
buckets[bucket] = _mean(members)
|
|
223
|
+
|
|
224
|
+
gauge_names = {name for row in rows.values() for name in row.gauges}
|
|
225
|
+
gauges = []
|
|
226
|
+
for name in sorted(gauge_names):
|
|
227
|
+
values = [value for row in rows.values() if (value := row.gauges.get(name)) is not None]
|
|
228
|
+
gauges.append(
|
|
229
|
+
GaugeStat(
|
|
230
|
+
name=name,
|
|
231
|
+
mean=_mean(values),
|
|
232
|
+
median=statistics.median(values) if values else None,
|
|
233
|
+
count=len(values),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
baseline_id = None
|
|
238
|
+
if baseline is not None:
|
|
239
|
+
baseline_id, baseline_rows = baseline
|
|
240
|
+
for case_id, row in rows.items():
|
|
241
|
+
base = baseline_rows.get(case_id)
|
|
242
|
+
if base is not None and base.overall is not None and row.overall is not None:
|
|
243
|
+
row.baseline_overall = base.overall
|
|
244
|
+
row.delta = round(row.overall - base.overall, 6)
|
|
245
|
+
|
|
246
|
+
return header.model_copy(
|
|
247
|
+
update={
|
|
248
|
+
"cases": sorted(rows.values(), key=lambda r: r.case_id),
|
|
249
|
+
"evaluators": evaluators,
|
|
250
|
+
"buckets": buckets,
|
|
251
|
+
"gauges": gauges,
|
|
252
|
+
"evaluator_columns": order_evaluator_columns(evaluator_names),
|
|
253
|
+
"baseline_id": baseline_id,
|
|
254
|
+
}
|
|
255
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from bisect import bisect_right
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, NamedTuple
|
|
7
|
+
|
|
8
|
+
from evalkit.contracts.atif import (
|
|
9
|
+
AtifAgent,
|
|
10
|
+
FinalMetrics,
|
|
11
|
+
Observation,
|
|
12
|
+
ObservationResult,
|
|
13
|
+
Step,
|
|
14
|
+
StepMetrics,
|
|
15
|
+
SubagentTrajectoryRef,
|
|
16
|
+
ToolCall,
|
|
17
|
+
Trajectory,
|
|
18
|
+
)
|
|
19
|
+
from evalkit.contracts.esf import EsfSpan, MappingDocument
|
|
20
|
+
from evalkit.core.traces.mapping import gen_ai_messages
|
|
21
|
+
|
|
22
|
+
ATIF_VERSION = "ATIF-v1.8"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _as_text(value: Any) -> str | None:
|
|
26
|
+
if value is None:
|
|
27
|
+
return None
|
|
28
|
+
if isinstance(value, str):
|
|
29
|
+
return value
|
|
30
|
+
return json.dumps(value, ensure_ascii=False, default=str)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _as_dict(value: dict[str, Any] | str) -> dict[str, Any]:
|
|
34
|
+
if isinstance(value, dict):
|
|
35
|
+
return value
|
|
36
|
+
try:
|
|
37
|
+
parsed = json.loads(value)
|
|
38
|
+
except ValueError:
|
|
39
|
+
return {"raw": value}
|
|
40
|
+
return parsed if isinstance(parsed, dict) else {"raw": value}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _output_text(value: Any) -> str | None:
|
|
44
|
+
messages = gen_ai_messages(value)
|
|
45
|
+
if messages is None:
|
|
46
|
+
return _as_text(value)
|
|
47
|
+
texts = [m.text for m in messages if m.role == "assistant" and m.text]
|
|
48
|
+
return texts[-1] if texts else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _Tree:
|
|
52
|
+
def __init__(self, spans: list[EsfSpan]):
|
|
53
|
+
self.by_id = {s.span_id: s for s in spans}
|
|
54
|
+
self.children: dict[str | None, list[EsfSpan]] = {}
|
|
55
|
+
for s in sorted(spans, key=lambda x: x.started_at):
|
|
56
|
+
parent = s.parent_id if s.parent_id in self.by_id else None
|
|
57
|
+
self.children.setdefault(parent, []).append(s)
|
|
58
|
+
|
|
59
|
+
def roots(self) -> list[EsfSpan]:
|
|
60
|
+
return self.children.get(None, [])
|
|
61
|
+
|
|
62
|
+
def subtree(self, span_id: str) -> list[EsfSpan]:
|
|
63
|
+
out: list[EsfSpan] = []
|
|
64
|
+
stack = [span_id]
|
|
65
|
+
while stack:
|
|
66
|
+
current = stack.pop()
|
|
67
|
+
for child in self.children.get(current, []):
|
|
68
|
+
out.append(child)
|
|
69
|
+
stack.append(child.span_id)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
def ancestors(self, span: EsfSpan) -> list[EsfSpan]:
|
|
73
|
+
out: list[EsfSpan] = []
|
|
74
|
+
parent = span.parent_id
|
|
75
|
+
while parent in self.by_id:
|
|
76
|
+
out.append(self.by_id[parent])
|
|
77
|
+
parent = self.by_id[parent].parent_id
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _answer_of(root: EsfSpan | None, spans: list[EsfSpan], rule: str) -> str | None:
|
|
82
|
+
if rule == "root":
|
|
83
|
+
return _output_text(root.output) if root is not None else None
|
|
84
|
+
name, _, key = rule.partition(".")
|
|
85
|
+
ordered = sorted(spans, key=lambda x: x.started_at)
|
|
86
|
+
named = next((s for s in ordered if s.name == name), None)
|
|
87
|
+
if named is None:
|
|
88
|
+
return None
|
|
89
|
+
if key and isinstance(named.output, dict):
|
|
90
|
+
return _as_text(named.output.get(key))
|
|
91
|
+
return _output_text(named.output)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _window(llms: list[EsfSpan], at: datetime) -> int | None:
|
|
95
|
+
index = bisect_right([s.started_at for s in llms], at) - 1
|
|
96
|
+
return index if index >= 0 else None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _result(
|
|
100
|
+
span: EsfSpan, call_id: str | None, refs: dict[str, list[SubagentTrajectoryRef]]
|
|
101
|
+
) -> ObservationResult:
|
|
102
|
+
return ObservationResult(
|
|
103
|
+
source_call_id=call_id,
|
|
104
|
+
content=_output_text(span.output) if span.kind == "agent" else _as_text(span.output),
|
|
105
|
+
subagent_trajectory_ref=refs.get(span.span_id),
|
|
106
|
+
extra={"is_error": span.error is not None},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _place(
|
|
111
|
+
steps: list[Step], outcomes: list[EsfSpan], llms: list[EsfSpan]
|
|
112
|
+
) -> tuple[dict[int, list[tuple[str | None, EsfSpan]]], list[EsfSpan]]:
|
|
113
|
+
owner = {c.tool_call_id: k for k, step in enumerate(steps) for c in step.tool_calls or []}
|
|
114
|
+
placed: dict[int, list[tuple[str | None, EsfSpan]]] = {}
|
|
115
|
+
claimed: set[str] = set()
|
|
116
|
+
by_time: list[EsfSpan] = []
|
|
117
|
+
for span in sorted(outcomes, key=lambda s: s.started_at):
|
|
118
|
+
call_id = span.tool_call_id
|
|
119
|
+
if call_id and call_id in owner and call_id not in claimed:
|
|
120
|
+
claimed.add(call_id)
|
|
121
|
+
placed.setdefault(owner[call_id], []).append((call_id, span))
|
|
122
|
+
else:
|
|
123
|
+
by_time.append(span)
|
|
124
|
+
pending = {
|
|
125
|
+
k: [c.tool_call_id for c in step.tool_calls or [] if c.tool_call_id not in claimed]
|
|
126
|
+
for k, step in enumerate(steps)
|
|
127
|
+
}
|
|
128
|
+
unplaced: list[EsfSpan] = []
|
|
129
|
+
for span in by_time:
|
|
130
|
+
k = _window(llms, span.started_at)
|
|
131
|
+
if k is None:
|
|
132
|
+
unplaced.append(span)
|
|
133
|
+
continue
|
|
134
|
+
placed.setdefault(k, []).append((pending[k].pop(0) if pending[k] else None, span))
|
|
135
|
+
return placed, unplaced
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _steps(
|
|
139
|
+
llms: list[EsfSpan],
|
|
140
|
+
outcomes: list[EsfSpan],
|
|
141
|
+
refs: dict[str, list[SubagentTrajectoryRef]],
|
|
142
|
+
) -> tuple[list[Step], list[EsfSpan]]:
|
|
143
|
+
steps: list[Step] = []
|
|
144
|
+
agent_steps: list[Step] = []
|
|
145
|
+
llms = sorted(llms, key=lambda s: s.started_at)
|
|
146
|
+
for i, call in enumerate(llms):
|
|
147
|
+
if i == 0 and call.input_messages:
|
|
148
|
+
for m in call.input_messages:
|
|
149
|
+
if m.role == "user":
|
|
150
|
+
steps.append(Step(step_id=0, source="user", message=m.text or ""))
|
|
151
|
+
elif m.role == "system":
|
|
152
|
+
steps.append(Step(step_id=0, source="system", message=m.text or ""))
|
|
153
|
+
assistant = next(
|
|
154
|
+
(m for m in reversed(call.output_messages or []) if m.role == "assistant"), None
|
|
155
|
+
)
|
|
156
|
+
tool_calls = [
|
|
157
|
+
ToolCall(tool_call_id=c.id, function_name=c.name, arguments=_as_dict(c.arguments))
|
|
158
|
+
for c in (assistant.tool_calls if assistant else None) or []
|
|
159
|
+
]
|
|
160
|
+
step = Step(
|
|
161
|
+
step_id=0,
|
|
162
|
+
timestamp=call.started_at.isoformat(),
|
|
163
|
+
source="agent",
|
|
164
|
+
message=(assistant.text if assistant else None) or "",
|
|
165
|
+
reasoning_content=assistant.reasoning if assistant else None,
|
|
166
|
+
model_name=call.model,
|
|
167
|
+
metrics=StepMetrics(
|
|
168
|
+
prompt_tokens=call.prompt_tokens,
|
|
169
|
+
completion_tokens=call.completion_tokens,
|
|
170
|
+
cached_tokens=call.cached_tokens,
|
|
171
|
+
cost_usd=call.cost_usd,
|
|
172
|
+
extra={"cache_creation_input_tokens": call.cache_write_tokens}
|
|
173
|
+
if call.cache_write_tokens
|
|
174
|
+
else None,
|
|
175
|
+
),
|
|
176
|
+
tool_calls=tool_calls or None,
|
|
177
|
+
)
|
|
178
|
+
steps.append(step)
|
|
179
|
+
agent_steps.append(step)
|
|
180
|
+
placed, unplaced = _place(agent_steps, outcomes, llms)
|
|
181
|
+
for k, pairs in placed.items():
|
|
182
|
+
order = {c.tool_call_id: j for j, c in enumerate(agent_steps[k].tool_calls or [])}
|
|
183
|
+
pairs.sort(
|
|
184
|
+
key=lambda p: (order.get(p[0], len(order)) if p[0] else len(order), p[1].started_at)
|
|
185
|
+
)
|
|
186
|
+
agent_steps[k].observation = Observation(
|
|
187
|
+
results=[_result(span, call_id, refs) for call_id, span in pairs]
|
|
188
|
+
)
|
|
189
|
+
return steps, unplaced
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _add[Num: (int, float)](total: Num | None, value: Num | None) -> Num | None:
|
|
193
|
+
if total is None:
|
|
194
|
+
return value
|
|
195
|
+
if value is None:
|
|
196
|
+
return total
|
|
197
|
+
return total + value
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _final_metrics(steps: list[Step], subagents: list[Trajectory]) -> FinalMetrics:
|
|
201
|
+
final = FinalMetrics(total_steps=len(steps))
|
|
202
|
+
for m in (s.metrics for s in steps if s.metrics is not None):
|
|
203
|
+
final.total_prompt_tokens = _add(final.total_prompt_tokens, m.prompt_tokens)
|
|
204
|
+
final.total_completion_tokens = _add(final.total_completion_tokens, m.completion_tokens)
|
|
205
|
+
final.total_cached_tokens = _add(final.total_cached_tokens, m.cached_tokens)
|
|
206
|
+
final.total_cost_usd = _add(final.total_cost_usd, m.cost_usd)
|
|
207
|
+
for sub in (s.final_metrics for s in subagents if s.final_metrics is not None):
|
|
208
|
+
final.total_prompt_tokens = _add(final.total_prompt_tokens, sub.total_prompt_tokens)
|
|
209
|
+
final.total_completion_tokens = _add(
|
|
210
|
+
final.total_completion_tokens, sub.total_completion_tokens
|
|
211
|
+
)
|
|
212
|
+
final.total_cached_tokens = _add(final.total_cached_tokens, sub.total_cached_tokens)
|
|
213
|
+
final.total_cost_usd = _add(final.total_cost_usd, sub.total_cost_usd)
|
|
214
|
+
return final
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class _Split(NamedTuple):
|
|
218
|
+
tree: _Tree
|
|
219
|
+
original_root: EsfSpan | None
|
|
220
|
+
main_root: EsfSpan | None
|
|
221
|
+
top_nested: list[EsfSpan]
|
|
222
|
+
main: list[EsfSpan]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _split(kept: list[EsfSpan]) -> _Split:
|
|
226
|
+
tree = _Tree(kept)
|
|
227
|
+
roots = tree.roots()
|
|
228
|
+
original_root = roots[0] if len(roots) == 1 else None
|
|
229
|
+
main_root = original_root
|
|
230
|
+
if main_root is None or main_root.kind != "agent":
|
|
231
|
+
top_agents = [
|
|
232
|
+
s
|
|
233
|
+
for s in kept
|
|
234
|
+
if s.kind == "agent" and not any(a.kind == "agent" for a in tree.ancestors(s))
|
|
235
|
+
]
|
|
236
|
+
if top_agents:
|
|
237
|
+
main_root = min(top_agents, key=lambda s: s.started_at)
|
|
238
|
+
nested_agent_ids = {s.span_id for s in kept if s.kind == "agent" and s is not main_root}
|
|
239
|
+
top_nested = [
|
|
240
|
+
s
|
|
241
|
+
for s in kept
|
|
242
|
+
if s.span_id in nested_agent_ids
|
|
243
|
+
and not any(a.span_id in nested_agent_ids for a in tree.ancestors(s))
|
|
244
|
+
]
|
|
245
|
+
nested_ids = {n.span_id for n in top_nested}
|
|
246
|
+
nested_ids |= {s.span_id for n in top_nested for s in tree.subtree(n.span_id)}
|
|
247
|
+
main = [s for s in kept if s.span_id not in nested_ids]
|
|
248
|
+
return _Split(tree, original_root, main_root, top_nested, main)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _main_llms(main: list[EsfSpan]) -> list[EsfSpan]:
|
|
252
|
+
return [s for s in main if s.kind == "llm"]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _excluded(mapping: MappingDocument | None) -> set[str]:
|
|
256
|
+
return set(mapping.exclude_spans) if mapping is not None else set()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def first_llm_spans(spans: list[EsfSpan], mapping: MappingDocument | None) -> list[EsfSpan]:
|
|
260
|
+
split = _split([s for s in spans if s.name not in _excluded(mapping)])
|
|
261
|
+
llms = sorted(_main_llms(split.main), key=lambda s: s.started_at)
|
|
262
|
+
nested = [
|
|
263
|
+
first
|
|
264
|
+
for n in split.top_nested
|
|
265
|
+
for first in first_llm_spans([n, *split.tree.subtree(n.span_id)], mapping)
|
|
266
|
+
]
|
|
267
|
+
return llms[:1] + nested
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def to_atif(
|
|
271
|
+
spans: list[EsfSpan],
|
|
272
|
+
mapping: MappingDocument | None,
|
|
273
|
+
*,
|
|
274
|
+
agent_name: str,
|
|
275
|
+
session_id: str | None,
|
|
276
|
+
mapping_version: int | None,
|
|
277
|
+
incomplete_ended_at: datetime | None = None,
|
|
278
|
+
) -> Trajectory:
|
|
279
|
+
kept = [s for s in spans if s.name not in _excluded(mapping)]
|
|
280
|
+
incomplete = sum(1 for s in kept if s.ended_at is None)
|
|
281
|
+
if incomplete_ended_at is not None:
|
|
282
|
+
kept = [
|
|
283
|
+
s.model_copy(update={"ended_at": incomplete_ended_at}) if s.ended_at is None else s
|
|
284
|
+
for s in kept
|
|
285
|
+
]
|
|
286
|
+
split = _split(kept)
|
|
287
|
+
tree, original_root, main_root = split.tree, split.original_root, split.main_root
|
|
288
|
+
top_nested, main = split.top_nested, split.main
|
|
289
|
+
tools = [
|
|
290
|
+
s for s in main if s.kind == "tool" and not any(a.kind == "tool" for a in tree.ancestors(s))
|
|
291
|
+
]
|
|
292
|
+
tool_ids = {t.span_id for t in tools}
|
|
293
|
+
refs: dict[str, list[SubagentTrajectoryRef]] = {}
|
|
294
|
+
outcomes = list(tools)
|
|
295
|
+
for n in top_nested:
|
|
296
|
+
ref = SubagentTrajectoryRef(trajectory_id=f"{n.trace_id}:{n.span_id}")
|
|
297
|
+
spawner = next((a for a in tree.ancestors(n) if a.span_id in tool_ids), n)
|
|
298
|
+
refs.setdefault(spawner.span_id, []).append(ref)
|
|
299
|
+
if spawner is n:
|
|
300
|
+
outcomes.append(n)
|
|
301
|
+
steps, unplaced = _steps(_main_llms(main), outcomes, refs)
|
|
302
|
+
rule = mapping.answer if mapping is not None else "root"
|
|
303
|
+
answer = _answer_of(original_root or main_root, kept, rule)
|
|
304
|
+
if answer and (not steps or steps[-1].tool_calls):
|
|
305
|
+
steps.append(Step(step_id=0, source="agent", message=answer))
|
|
306
|
+
for i, step in enumerate(steps, 1):
|
|
307
|
+
step.step_id = i
|
|
308
|
+
subagents = [
|
|
309
|
+
to_atif(
|
|
310
|
+
[n, *tree.subtree(n.span_id)],
|
|
311
|
+
mapping,
|
|
312
|
+
agent_name=n.agent_name or n.name,
|
|
313
|
+
session_id=session_id,
|
|
314
|
+
mapping_version=mapping_version,
|
|
315
|
+
)
|
|
316
|
+
for n in top_nested
|
|
317
|
+
]
|
|
318
|
+
ordered_main = sorted(main, key=lambda x: x.started_at)
|
|
319
|
+
first_model = next((s.model for s in ordered_main if s.kind == "llm" and s.model), None)
|
|
320
|
+
extra: dict[str, Any] = {"trajectory_source": "otel", "mapping_version": mapping_version}
|
|
321
|
+
if incomplete:
|
|
322
|
+
extra["incomplete_spans"] = incomplete
|
|
323
|
+
unplaced_tools = [s.name for s in unplaced if s.kind == "tool"]
|
|
324
|
+
if unplaced_tools:
|
|
325
|
+
extra["unplaced_tool_spans"] = unplaced_tools
|
|
326
|
+
anchor = main_root or (kept[0] if kept else None)
|
|
327
|
+
return Trajectory(
|
|
328
|
+
schema_version=ATIF_VERSION,
|
|
329
|
+
session_id=session_id,
|
|
330
|
+
trajectory_id=f"{anchor.trace_id}:{anchor.span_id}" if anchor else None,
|
|
331
|
+
agent=AtifAgent(name=agent_name, model_name=first_model),
|
|
332
|
+
steps=steps,
|
|
333
|
+
final_metrics=_final_metrics(steps, subagents),
|
|
334
|
+
subagent_trajectories=subagents or None,
|
|
335
|
+
extra=extra,
|
|
336
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from evalkit.contracts.enums import TraceImportStatus
|
|
7
|
+
from evalkit.contracts.esf import MappingDocument
|
|
8
|
+
from evalkit.core.runner import error_text
|
|
9
|
+
from evalkit.core.traces.mapping import session_id_of
|
|
10
|
+
from evalkit.core.traces.raw import RawSpan
|
|
11
|
+
from evalkit.core.traces.sources import TraceSourceClient
|
|
12
|
+
|
|
13
|
+
IMPORT_LEASE_SECONDS = 180
|
|
14
|
+
IMPORT_STOPPED_ERROR = "the server stopped while importing"
|
|
15
|
+
ACTIVE_IMPORT = frozenset({TraceImportStatus.RUNNING, TraceImportStatus.STOPPING})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ImportJob(BaseModel):
|
|
19
|
+
id: int
|
|
20
|
+
agent_id: int
|
|
21
|
+
trace_source_id: int
|
|
22
|
+
project: str
|
|
23
|
+
until: datetime
|
|
24
|
+
resume_from: datetime
|
|
25
|
+
resume_trace_ids: list[str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ImportStore(Protocol):
|
|
29
|
+
async def job(self, import_id: int) -> ImportJob: ...
|
|
30
|
+
|
|
31
|
+
async def advance(
|
|
32
|
+
self,
|
|
33
|
+
import_id: int,
|
|
34
|
+
holder: str,
|
|
35
|
+
*,
|
|
36
|
+
resume_from: datetime,
|
|
37
|
+
resume_trace_ids: list[str],
|
|
38
|
+
traces: int,
|
|
39
|
+
spans: int,
|
|
40
|
+
sample_session: str | None,
|
|
41
|
+
) -> TraceImportStatus | None: ...
|
|
42
|
+
|
|
43
|
+
async def renew(self, import_id: int, holder: str) -> TraceImportStatus | None: ...
|
|
44
|
+
|
|
45
|
+
async def finish(
|
|
46
|
+
self, import_id: int, holder: str, status: TraceImportStatus, error: str | None = None
|
|
47
|
+
) -> None: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SpanWriter(Protocol):
|
|
51
|
+
async def upsert(
|
|
52
|
+
self, agent_id: int, spans: list[RawSpan], *, external_trace_source_id: int | None = None
|
|
53
|
+
) -> int: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def run_import(
|
|
57
|
+
import_id: int,
|
|
58
|
+
holder: str,
|
|
59
|
+
client: TraceSourceClient,
|
|
60
|
+
store: ImportStore,
|
|
61
|
+
spans: SpanWriter,
|
|
62
|
+
mapping: MappingDocument | None,
|
|
63
|
+
) -> None:
|
|
64
|
+
job = await store.job(import_id)
|
|
65
|
+
boundary, written = job.resume_from, set(job.resume_trace_ids)
|
|
66
|
+
async for page in client.trace_pages(job.project, job.resume_from, job.until):
|
|
67
|
+
fresh = [tid for tid, start in page.traces if start != boundary or tid not in written]
|
|
68
|
+
span_count, sample = 0, None
|
|
69
|
+
if fresh:
|
|
70
|
+
async for batch in client.trace_spans(job.project, fresh):
|
|
71
|
+
for span in batch:
|
|
72
|
+
span.session_id_hint = session_id_of(span.attributes, mapping)
|
|
73
|
+
await spans.upsert(
|
|
74
|
+
job.agent_id, batch, external_trace_source_id=job.trace_source_id
|
|
75
|
+
)
|
|
76
|
+
span_count += len(batch)
|
|
77
|
+
roots = [s.session_id_hint for s in batch if s.parent_span_id is None]
|
|
78
|
+
sample = next((r for r in reversed(roots) if r), sample)
|
|
79
|
+
if await store.renew(import_id, holder) is None:
|
|
80
|
+
return
|
|
81
|
+
at_last = {tid for tid, start in page.traces if start == page.last_start}
|
|
82
|
+
written = written | at_last if page.last_start == boundary else at_last
|
|
83
|
+
boundary = page.last_start
|
|
84
|
+
state = await store.advance(
|
|
85
|
+
import_id,
|
|
86
|
+
holder,
|
|
87
|
+
resume_from=boundary,
|
|
88
|
+
resume_trace_ids=sorted(written),
|
|
89
|
+
traces=len(fresh),
|
|
90
|
+
spans=span_count,
|
|
91
|
+
sample_session=sample,
|
|
92
|
+
)
|
|
93
|
+
if state is None:
|
|
94
|
+
return
|
|
95
|
+
if state is TraceImportStatus.STOPPING:
|
|
96
|
+
await store.finish(import_id, holder, TraceImportStatus.STOPPED)
|
|
97
|
+
return
|
|
98
|
+
await store.finish(import_id, holder, TraceImportStatus.DONE)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def guarded_import(
|
|
102
|
+
import_id: int,
|
|
103
|
+
holder: str,
|
|
104
|
+
client: TraceSourceClient,
|
|
105
|
+
store: ImportStore,
|
|
106
|
+
spans: SpanWriter,
|
|
107
|
+
mapping: MappingDocument | None,
|
|
108
|
+
) -> None:
|
|
109
|
+
try:
|
|
110
|
+
await run_import(import_id, holder, client, store, spans, mapping)
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
await store.finish(import_id, holder, TraceImportStatus.FAILED, error_text(exc))
|
|
113
|
+
raise
|