gooddata-eval 1.77.1.dev1__tar.gz → 1.78.0__tar.gz
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.
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/AGENTS.md +5 -2
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/PKG-INFO +192 -20
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/README.md +190 -18
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/pyproject.toml +2 -2
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/cli/agentic_runner.py +30 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/cli/main.py +4 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/alert_skill.py +131 -17
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/anomaly_detection.py +7 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/conversation.py +14 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/dashboard_skill.py +6 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/document_skill.py +7 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/general_question.py +11 -2
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/guardrail.py +11 -2
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/kda_skill.py +7 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/metric_skill.py +7 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/search_tool.py +9 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/visualization.py +8 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/what_if.py +7 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/chat/sse_client.py +15 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_llm_judge.py +1 -0
- gooddata_eval-1.78.0/src/gooddata_eval/core/interaction.py +57 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/models.py +6 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/reporting/console.py +52 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/reporting/html_report.py +40 -5
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/reporting/json_report.py +19 -0
- gooddata_eval-1.78.0/src/gooddata_eval/core/reporting/report_template.html +1960 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/runner.py +67 -1
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/summary/http_client.py +11 -1
- gooddata_eval-1.78.0/src/gooddata_eval/core/timing.py +184 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_alert_skill.py +212 -1
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_anomaly_detection.py +3 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_conversation.py +3 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_dashboard_skill.py +3 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_document_skill.py +5 -1
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_general_question.py +8 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_guardrail.py +6 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_kda_skill.py +3 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_metric_skill.py +12 -4
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_runner.py +170 -1
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_search_tool.py +5 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_visualization.py +5 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_what_if.py +3 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_cli.py +55 -0
- gooddata_eval-1.78.0/tests/test_html_report.py +1364 -0
- gooddata_eval-1.78.0/tests/test_interaction.py +35 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_reporting.py +159 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_runner.py +200 -1
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_sse_client.py +81 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_summary_client.py +6 -0
- gooddata_eval-1.78.0/tests/test_timing.py +270 -0
- gooddata_eval-1.77.1.dev1/src/gooddata_eval/core/reporting/report_template.html +0 -452
- gooddata_eval-1.77.1.dev1/src/gooddata_eval/core/timing.py +0 -74
- gooddata_eval-1.77.1.dev1/tests/test_html_report.py +0 -202
- gooddata_eval-1.77.1.dev1/tests/test_timing.py +0 -93
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/.gitignore +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/CLAUDE.md +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/LICENSE.txt +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/Makefile +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/scripts/verify_guardrail_refusal_criteria.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/_output.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_gate.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/chat/render.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/config.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/summary.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/granularity.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/_env.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/client.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/observations.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/scoring.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/__init__.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/_fake_langfuse.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/conftest.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_gate.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_join_trace.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_langfuse_trace.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_observe_experiment.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_run_context.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_alert_skill_evaluator.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_chat_render.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_connection.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_fake_langfuse.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_from_insights.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_guardrail_criteria.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_client.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_e2e_fake_server.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_env.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_experiment.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_item_scope.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_observations.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_otlp.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_source.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_maql_normalize.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_models.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_trace_linker.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_visualization_evaluator.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_workspace.py +0 -0
- {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tox.ini +0 -0
|
@@ -168,8 +168,11 @@ A gitignored `.env` at the repo root is the normal place for these; load it with
|
|
|
168
168
|
**Adding an evaluator is a registry change, not a naming convention.** Single-shot kinds go
|
|
169
169
|
into `_EAGER_EVALUATORS`, or `_LAZY_EVALUATOR_MODULES` plus `_LAZY_EVALUATOR_CLASSES`, in
|
|
170
170
|
`core/evaluators/__init__.py`. Agentic kinds need the string added to `AGENTIC_TEST_KINDS`
|
|
171
|
-
and a new branch in `_dispatch_agentic
|
|
172
|
-
|
|
171
|
+
and a new branch in `_dispatch_agentic`, and must also be classified in `core/interaction.py`:
|
|
172
|
+
add it to `SINGLE_TURN_AGENTIC_KINDS` (one chat message per run) or `MULTI_TURN_KINDS` (a
|
|
173
|
+
simulated user or several turns). That decides which latency section its items land in;
|
|
174
|
+
`test_every_agentic_kind_is_classified_on_purpose` fails until it is done. Test file naming
|
|
175
|
+
follows the capability, but naming a test file correctly registers nothing.
|
|
173
176
|
|
|
174
177
|
**Parallel-safety is a reviewed allowlist, and getting it wrong corrupts results.**
|
|
175
178
|
`WORKSPACE_MUTATING_TEST_KINDS` is computed as `AGENTIC_TEST_KINDS - PARALLEL_SAFE_TEST_KINDS`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.78.0
|
|
4
4
|
Summary: Evaluate the GoodData AI agent against your own questions and models.
|
|
5
5
|
Project-URL: Source, https://github.com/gooddata/gooddata-python-sdk
|
|
6
6
|
Author-email: GoodData <support@gooddata.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
17
17
|
Classifier: Topic :: Software Development
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist: gooddata-sdk~=1.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.78.0
|
|
21
21
|
Requires-Dist: httpx<1.0,>=0.27
|
|
22
22
|
Requires-Dist: jsonpatch<2.0,>=1.33
|
|
23
23
|
Requires-Dist: orjson<4.0.0,>=3.9.15
|
|
@@ -263,6 +263,11 @@ The JSON report always uses the nested multi-model shape:
|
|
|
263
263
|
|
|
264
264
|
Winner is selected by **pass rate → quality score → latency** (lower latency wins all-equal ties).
|
|
265
265
|
|
|
266
|
+
The HTML report orders runs by when they started (`summary.started_at`, an ISO 8601 UTC timestamp that `gd-eval run` writes
|
|
267
|
+
into each run's `summary`, `null` when a run was never timed), so the order the files are passed to
|
|
268
|
+
`gd-eval report` does not decide which run is the latest. A report without it falls back to the date a run's file is
|
|
269
|
+
named for (`2026-09-28.json`), and if any run has neither it keeps the order the files were given in.
|
|
270
|
+
|
|
266
271
|
Each item reports **how many of its runs passed**, not only whether one did:
|
|
267
272
|
|
|
268
273
|
```json
|
|
@@ -337,13 +342,73 @@ The other six agentic kinds report `0.0` for the first three. Trace linking itse
|
|
|
337
342
|
runner rather than by `evaluate_agentic_*`, so a direct library caller sees `langfuse_s: 0.0` even though its
|
|
338
343
|
linking ran. Pass `TAVERN_E2E_SKIP_TRACE_LINK=1` to opt out of linking altogether.
|
|
339
344
|
|
|
345
|
+
### Latency: single-shot and multi-turn
|
|
346
|
+
|
|
347
|
+
`avg_latency_s` is one number over a mixed population. The report instead keeps two kinds of item apart, by
|
|
348
|
+
their eval kind, and states latency for each:
|
|
349
|
+
|
|
350
|
+
- **Single-shot** — kinds that send exactly one chat message per run: every non-agentic kind, plus
|
|
351
|
+
`agentic_general_question`, `agentic_guardrail` and `agentic_search`.
|
|
352
|
+
- **Multi-turn** — kinds that drive a simulated user or fixture turns: `agentic_visualization` (and
|
|
353
|
+
`vis_agentic`), `agentic_metric_skill`, `agentic_alert_skill`, `agentic_dashboard_skill`,
|
|
354
|
+
`agentic_document_skill`, `agentic_kda_skill`, `agentic_anomaly_detection`, `agentic_what_if` and
|
|
355
|
+
`agentic_conversation`.
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
"summary": {
|
|
359
|
+
"by_mode": {
|
|
360
|
+
"single_shot": {"total": 12, "passed": 10,
|
|
361
|
+
"traces_s": {"n": 24, "mean": 5.39, "trimmed_mean": 5.36, "min": 3.88, "p50": 5.33, "p70": 6.23, "p90": 6.82, "p99": 7.18, "max": 7.22}},
|
|
362
|
+
"multi_turn": {"total": 6, "passed": 4,
|
|
363
|
+
"traces_s": {"n": 45, "mean": 6.91, "trimmed_mean": 6.84, "min": 2.78, "p50": 6.52, "p70": 8.03, "p90": 10.79, "p99": 13.57, "max": 14.25},
|
|
364
|
+
"runs_s": {"n": 15, "mean": 23.55, "trimmed_mean": 23.31, "min": 12.64, "p50": 21.23, "p70": 28.52, "p90": 33.02, "p99": 36.94, "max": 37.51}}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`by_mode` is in each run's `summary` and in its `comparison` entry. `total` and `passed` count the evaluated
|
|
370
|
+
items of that mode (skipped ones belong to neither). Every distribution is `null` when nothing was timed.
|
|
371
|
+
Each item also states its `mode`, its `trace_latencies_s` and its `run_latencies_s`.
|
|
372
|
+
|
|
373
|
+
- **Per trace is the main figure.** gen-ai starts one trace per chat message, so a trace is one chat turn.
|
|
374
|
+
Its latency is that turn's response time, from sending the message to the end of its stream
|
|
375
|
+
(`turn_wall_clock_sec`), recorded where the chat client completes the turn. It holds none of the LLM judge,
|
|
376
|
+
the simulated user or conversation create/delete. `dashboard_summary` is timed the same way around its one
|
|
377
|
+
request. A turn cut off by its turn or item deadline, or one that ended without a final answer (the agent's
|
|
378
|
+
iteration or token limit, a content filter), still counts, with the time it ran for: those are the slowest
|
|
379
|
+
turns, and the run that scored them is in the per-run figures too. A turn that ended in any other error (an
|
|
380
|
+
error event, or a 503 that outlasted its retries) does not: the time is that of a failed request, not a
|
|
381
|
+
response, and would only add a fast outlier. On the single-shot path such a turn ends its item as an error, so
|
|
382
|
+
it has no per-run figure either. An item that errored outside its evaluator's own failure reporting
|
|
383
|
+
contributes neither.
|
|
384
|
+
- **Single-shot** runs are exactly one trace, so `traces_s` is also the latency of a run.
|
|
385
|
+
- **Multi-turn** runs span several traces, so they get both views: `traces_s` over every turn of every
|
|
386
|
+
conversation (comparable with single-shot), and `runs_s`, the wall clock of each whole conversation from just
|
|
387
|
+
before its first message to the end of its scoring — every turn, simulated-user reply and judge call, but not
|
|
388
|
+
conversation create/delete. `agentic_conversation` drives its fixture once and contributes one run per item.
|
|
389
|
+
The two single-shot and multi-turn populations are never mixed.
|
|
390
|
+
- **Percentiles** use linear interpolation between closest ranks (NumPy's default), so `p50` is the median and
|
|
391
|
+
the report carries no separate median field. On a small sample the high percentiles sit at or near the
|
|
392
|
+
maximum: P99 over fewer than 100 samples is effectively the slowest one, which is why `n` always travels with it.
|
|
393
|
+
- **Trimmed mean** (`trimmed_mean`) is the average of the middle 80%: the fastest 10% and the slowest 10% of the
|
|
394
|
+
samples are dropped from each end (`n * 10 // 100` of them, so 25 samples drop two each) before averaging. It is
|
|
395
|
+
not the median, which is one middle value (`p50`). It is `null` below 10 samples, where there is nothing to
|
|
396
|
+
trim and the figure would only repeat `mean`.
|
|
397
|
+
- **Older reports** have no `by_mode`. The HTML report says so and nothing is backfilled.
|
|
398
|
+
|
|
399
|
+
`avg_latency_s` and the other existing fields are unchanged. The console prints each run's pass counts per
|
|
400
|
+
mode and one `Latency, <population> (n=...)` line per distribution beneath its summary line, and Trace
|
|
401
|
+
P50/P90/P99 columns (single-shot) in the multi-model comparison.
|
|
402
|
+
|
|
340
403
|
---
|
|
341
404
|
|
|
342
405
|
## `gd-eval report`
|
|
343
406
|
|
|
344
407
|
Turns JSON report(s) into one HTML file you can actually navigate. No server, no
|
|
345
408
|
credentials, no external assets — it opens over `file://`, attaches to a Jira issue and
|
|
346
|
-
survives a Slack thread.
|
|
409
|
+
survives a Slack thread. It follows GoodData's product look (colours, type, status chips). The faces are Inter, with
|
|
410
|
+
Funnel Display for headings, named but never loaded, so a viewer without them gets the system sans and the file stays
|
|
411
|
+
self-contained.
|
|
347
412
|
|
|
348
413
|
```bash
|
|
349
414
|
# one run
|
|
@@ -360,27 +425,134 @@ gd-eval report results.json -o customer.html --redact
|
|
|
360
425
|
|---|---|
|
|
361
426
|
| `-o, --out PATH` | Where to write the HTML. Required. |
|
|
362
427
|
| `--title TEXT` | Title shown in the report header. |
|
|
363
|
-
| `--redact` | Drop conversation/response ids and raw reasoning, and rename models to `Model A`, `Model B`, … Pass rate, per-item results, questions and latency survive. |
|
|
364
|
-
|
|
365
|
-
The report is a *view* over the JSON
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
428
|
+
| `--redact` | Drop conversation/response ids and raw reasoning, and rename models to `Model A`, `Model B`, … A model keeps its letter across all its runs, which are then numbered (`Run 1 · Model A`, `Run 2 · Model A`). Pass rate, per-item results, questions and latency survive. |
|
|
429
|
+
|
|
430
|
+
The report is a *view* over the JSON. Unfiltered, every figure on it is read from the report, so it cannot
|
|
431
|
+
disagree with the console or the JSON. Narrow the selection with a filter and the figures are recomputed in the
|
|
432
|
+
page from the selected items' own records, by the same rules gd-eval uses. The page reads top to bottom as a
|
|
433
|
+
document, in plain words: a *test case* is one prompt given to the assistant, an *attempt* is one go at it, and the
|
|
434
|
+
two formats are *single-question* tests (one answer, no follow-up) and *conversation* tests (a back-and-forth with a
|
|
435
|
+
simulated user), which are `single_shot` and `multi_turn` in the JSON.
|
|
436
|
+
|
|
437
|
+
- **A masthead** with the report's title (`--title`, or "Run history", "Run comparison", "Model comparison" or
|
|
438
|
+
"Run results" plus the model and workspace), when it was generated in the reader's own time zone, and a **Run setup** tooltip listing what
|
|
439
|
+
is the same in every run (model, provider, workspace, dataset, test cases, attempts per test case, pass rule) and
|
|
440
|
+
what differs. Results from different setups are not directly comparable, so a caution icon on a latency row and the
|
|
441
|
+
*New* / *Removed* marks in the test case table say where two runs stopped being alike.
|
|
442
|
+
- **View by** — a dropdown above the filters that lays the same report out two ways. **Date** is the layout the
|
|
443
|
+
rest of this list describes: a column per run, oldest to latest by start time, with change lines, sparklines and the latency line
|
|
444
|
+
chart. **Model** compares language models instead: a column per model, ranked best first by the console's winner
|
|
445
|
+
rule (pass rate, then quality, then the quicker average), with no time axis. Where a model has several runs its
|
|
446
|
+
latest stands for it, so each model is read at its most recent state. The Run filter becomes a Model filter, the hint
|
|
447
|
+
beside the dropdown says what is being compared (and names the dates when the compared runs differ in date), and the
|
|
448
|
+
Run setup tooltip does not flag the model itself as a difference. Every report opens on Date, including one that
|
|
449
|
+
compares several models (`gd-eval run --model a --model b --html`); choose Model to compare them. A single run has
|
|
450
|
+
nothing to switch between, so the dropdown is hidden. In the Model view the page reads, top to bottom:
|
|
451
|
+
- **Results** — one row per model, best pass rate first, with a bar of how its evaluated test cases ended (passed
|
|
452
|
+
green, failed red, could not be graded orange), so the green part is the pass rate beside it, and the **Median
|
|
453
|
+
latency** of one attempt (half the attempts were faster, half slower). A list under the table says what each term means. The counts and the
|
|
454
|
+
caution icon (a model evaluated on different test cases than the one with the most) are the date view's Outcomes
|
|
455
|
+
table. There are no headline cards by model: the table is the first thing on the page.
|
|
456
|
+
- **Quality against speed** — each model as a point: the share of test cases it passed against the median time of
|
|
457
|
+
one attempt, the same pass rate as the Results table and the same median as the latency tables. Up and to the left is better. Time runs from zero;
|
|
458
|
+
the pass rate uses the range the models span. It needs two models with timings.
|
|
459
|
+
- **By category** — a heat map of the pass rate per category and model, the best model in each row marked, the
|
|
460
|
+
categories where the models differ most first. Every cell prints its share and its counts, so the shade is never the
|
|
461
|
+
only signal. This is where a model that wins overall but loses on one category shows.
|
|
462
|
+
- **Consistency across attempts** — per model, the share of test cases that passed on at least one attempt (pass@K,
|
|
463
|
+
the light bar) beside the share that passed on every attempt (pass^K, the dark bar), and how many passed only some.
|
|
464
|
+
A long gap is a model that can do the task but not reliably. It appears only for a report that recorded
|
|
465
|
+
`pass_power_k` and attempted a test case more than once.
|
|
466
|
+
- **Latency sections** — the same four percentiles, one row per model, quickest first, with a bar per figure drawn
|
|
467
|
+
from zero beside the numbers. The quickest and slowest model in each column are marked as in the date view.
|
|
468
|
+
- **Test cases** — one result column per model in ranked order, a response-time column per model, and an **Across
|
|
469
|
+
models** column in place of *Since previous run*: *Mixed* (some models passed and some did not; hover to see who),
|
|
470
|
+
*All pass* or *All fail*. A skipped or ungraded test case is left out of the verdict. The table opens on the
|
|
471
|
+
*Mixed* ones, the test cases where the models differ.
|
|
472
|
+
- **Filters in two tiers** — the **Filter the report** bar at the top (category, format, result and run) decides what
|
|
473
|
+
the whole page covers: the Results cards, the outcomes table, the latency tables and the test case table all follow it, so the runs stay
|
|
474
|
+
comparable. The Test cases section repeats those four and adds the search box and the expression; they narrow the
|
|
475
|
+
table only, inside whatever the top bar allows, and only offer what the top bar leaves. Category, result and run are
|
|
476
|
+
attribute filters like the product's: a search box, an All row, the values with an **Only** shortcut and a count,
|
|
477
|
+
then Cancel and Apply. Changes are staged in the popup and take effect on Apply; closing it any other way discards
|
|
478
|
+
them. Nothing ticked means all. The run filter chooses which runs are shown; a result matches a test case when any of
|
|
479
|
+
the runs in play has it (so "Failed" over previous and latest finds every test case that broke in either), and the
|
|
480
|
+
expression is evaluated once per run in play with the same any-run rule. A filtered page states how many test cases
|
|
481
|
+
it covers, and each tier has its own Reset.
|
|
482
|
+
- **Results** — three headline cards for the latest run: test cases passed (of those evaluated: skipped ones are not
|
|
483
|
+
counted, as in the console's percentage), the average quality score and the median time per attempt. That last one
|
|
484
|
+
pools every format on the page and counts whole attempts, grading included, so it matches neither per-response latency table
|
|
485
|
+
below it; its tooltip says which formats it covers. With an earlier run each card carries one change line in words, "down from 86% on Sep 28", rather
|
|
486
|
+
than a signed number. The line is green when that direction is good and red when it is not, with a chevron and
|
|
487
|
+
screen-reader text (*Better* / *Worse*) so colour is not the only signal. From three runs each card also has a
|
|
488
|
+
sparkline of every run: hover a point for its value, click it to compare the latest run with that one instead of the
|
|
489
|
+
previous. The dot being compared with is drawn white, and a key under the cards says so (latest run, compared with,
|
|
490
|
+
other runs) and that a click on any other dot moves the comparison; it shows only when a sparkline does. A pick that
|
|
491
|
+
a filter has removed, or that has become the latest run, falls back to the previous run. The
|
|
492
|
+
pass-rate card links to the test case table to see *which* ones changed; the split by format lives in that table's
|
|
493
|
+
filters.
|
|
494
|
+
- **Outcomes** — how every test case ended, as a table with one row per run, newest first, in the same layout as the
|
|
495
|
+
latency tables. A run is identified by three columns: the **Run date**, the day and time it started (`summary.started_at`,
|
|
496
|
+
shown in the reader's own time zone, the UTC stamp on hover; a report written before it existed shows the date its file is
|
|
497
|
+
named for, without a time), and the **Run name**, the name the run was given, as given, so a run named `2026-09-28` reads
|
|
498
|
+
`2026-09-28` here although the charts style it as "Sep 28", and the **Model** it evaluated, its provider on hover. Then **Passed**, **Failed**, **Could not be graded** (an error left it without a verdict) and
|
|
499
|
+
**Skipped** (not run), then the **Pass rate**. The four are the values of the Result filter, and the pass rate is
|
|
500
|
+
the card's: passed out of the evaluated test cases, skipped ones left out, as in the console. The highest pass rate
|
|
501
|
+
is marked green with an up chevron and the lowest red with a down chevron, and the latest run is marked in lime. A
|
|
502
|
+
caution icon flags a run evaluated over a different number of test cases than the one before, because its pass
|
|
503
|
+
rate is then not a like-for-like change. The counts are read from the run's `summary` (`passed`, `failed`,
|
|
504
|
+
`errored`, `skipped`); a filtered view recounts them from the selected test cases, and a report written before
|
|
505
|
+
`summary.failed` existed shows what the other three leave.
|
|
506
|
+
- **Latency sections** — **Single-question tests · response time** first, then **Conversation tests**, which shows
|
|
507
|
+
both per response (each assistant reply, one gen-ai trace) and per conversation (start to finish, with the
|
|
508
|
+
simulated user and the judge). Each opens with a **line chart** of the median (P50), P70, P90 and P99 across the
|
|
509
|
+
runs, oldest on the left and the latest on the right, drawn from zero so line heights compare honestly; the lines
|
|
510
|
+
share one blue that darkens towards the tail. Hover or focus a run for its crosshair and every figure, in a tooltip
|
|
511
|
+
that says how many responses it rests on. A line needs two runs with timings, so with one run the chart is left
|
|
512
|
+
out. End labels name each line and give its latest value, and are dropped when two would touch. Under the chart a
|
|
513
|
+
table has one row per run, newest first, identified by the same **Run date**, **Run name** and **Model** columns as the outcomes table, with the same four figures. The quickest run in each column is marked
|
|
514
|
+
green and the slowest red, each with a chevron (down for least time, up for most) so colour is not the only
|
|
515
|
+
signal, and the latest run is marked in lime. A caution icon flags a run timed on a different number of samples
|
|
516
|
+
than the one before. Unfiltered they are read from `by_mode`; a filtered view recomputes them in the page by the
|
|
517
|
+
same rules. The fastest and slowest single response, the average and the trimmed average stay in the JSON (P99
|
|
518
|
+
over a few dozen samples sits close to the slowest one, which is why `n` travels with it in the tooltip and
|
|
519
|
+
the cell hint). Seconds read with one decimal and no redundant ".0" (`3s`, `4.6s`).
|
|
520
|
+
- **A test case table**, newest first: the latest run's result, a **Since previous run** column, then the earlier runs
|
|
521
|
+
going back in time (click any result to open the drawer on that run). Each run's column is headed by its run date over
|
|
522
|
+
its run name, one to a line (hover the heading for the full run label); by model the columns are headed by the model. The change column reads *Regressed* (passed in
|
|
523
|
+
the previous run, not in the latest), *Fixed*, *Flaky* (flipped between pass and fail more than once), *New*
|
|
524
|
+
(in no earlier run) or *Removed* (in an earlier run, gone from the latest). A test case skipped in the latest run
|
|
525
|
+
has no label, since the change would describe earlier runs. With several runs the table opens on the regressions. Two time columns
|
|
526
|
+
keep the populations apart: **Response time** is the median of the test case's assistant responses, and **Time per
|
|
527
|
+
attempt** is the mean wall-clock of a whole attempt (the responses plus the judge and, for conversations, the
|
|
528
|
+
simulated user). With more than two runs shown they follow the latest run only. Hover a value to see every sample
|
|
529
|
+
behind it; the drawer labels its two latency chips the same way. A report written before per-trace timing has no
|
|
530
|
+
response figure and shows `avg_latency_s` as the attempt figure.
|
|
531
|
+
- **An expression filter**, folded under *Advanced: filter by a check expression* in the Test cases section, for
|
|
532
|
+
cross-cutting questions the fixed filters can't anticipate, e.g. `d.filter_ranking_score === false` or
|
|
372
533
|
`d.expected_metric_uris.length > 1 && !d.metrics_correct`. Available variables:
|
|
373
|
-
`d` (the
|
|
374
|
-
run), `q` (question), `kind
|
|
375
|
-
- **The conversation**, when the
|
|
534
|
+
`d` (the checks of the run being read, its `detail`), `it` (that run's result), `i` (the row, `i.per[label]` for any
|
|
535
|
+
run), `q` (question), `kind` (category id).
|
|
536
|
+
- **The conversation**, when the test case ran the agentic multi-turn path — every turn in
|
|
376
537
|
order, with the simulated user marked apart from a real question, so you can see
|
|
377
538
|
whether the agent got there or was handed the answer.
|
|
378
|
-
- **A per-
|
|
379
|
-
|
|
539
|
+
- **A per-test-case drawer** — checks as pass/fail chips, expected vs actual side by side,
|
|
540
|
+
the latency timeline, conversation/response ids.
|
|
380
541
|
- **A latency timeline** from `detail.latency_breakdown`, in execution order, one bar per
|
|
381
|
-
step.
|
|
382
|
-
reasoning step was summarised from, or a tool call's arguments and result from
|
|
383
|
-
`detail.tool_calls`.
|
|
542
|
+
step. Every step has a chevron that opens its full record in place, joined by `index`: the
|
|
543
|
+
paragraph a reasoning step was summarised from, or a tool call's arguments and result from
|
|
544
|
+
`detail.tool_calls`. A reasoning paragraph's markdown `**title**` is shown as bold text,
|
|
545
|
+
not as asterisks. Any number of steps can be open at once, and the chevron above the list
|
|
546
|
+
opens or closes all of them. The reasoning therefore lives on the timeline; a separate
|
|
547
|
+
Reasoning section appears only for paragraphs no step points at (or when a run recorded no
|
|
548
|
+
timeline).
|
|
549
|
+
- **A print stylesheet** that drops the filters and the drawer, so "Save as PDF" gives a clean page.
|
|
550
|
+
|
|
551
|
+
A report that merges several files names each run after its file, and a file named for its date (`2026-09-28.json`)
|
|
552
|
+
reads as "Sep 28". A model is read from the run itself (`model` and the provider), never from the file name, so files
|
|
553
|
+
named for their model work as well as files named for their date. In the Date view the page treats the runs as one
|
|
554
|
+
model followed over time; when a report holds several models it adds the model to each run's name and points at
|
|
555
|
+
**Model** (see above) for the comparison.
|
|
384
556
|
|
|
385
557
|
`--redact` additionally drops `transcript` and `tool_calls`: the exchange shows that the
|
|
386
558
|
simulated user is primed with the expected output, and a tool result carries
|
|
@@ -234,6 +234,11 @@ The JSON report always uses the nested multi-model shape:
|
|
|
234
234
|
|
|
235
235
|
Winner is selected by **pass rate → quality score → latency** (lower latency wins all-equal ties).
|
|
236
236
|
|
|
237
|
+
The HTML report orders runs by when they started (`summary.started_at`, an ISO 8601 UTC timestamp that `gd-eval run` writes
|
|
238
|
+
into each run's `summary`, `null` when a run was never timed), so the order the files are passed to
|
|
239
|
+
`gd-eval report` does not decide which run is the latest. A report without it falls back to the date a run's file is
|
|
240
|
+
named for (`2026-09-28.json`), and if any run has neither it keeps the order the files were given in.
|
|
241
|
+
|
|
237
242
|
Each item reports **how many of its runs passed**, not only whether one did:
|
|
238
243
|
|
|
239
244
|
```json
|
|
@@ -308,13 +313,73 @@ The other six agentic kinds report `0.0` for the first three. Trace linking itse
|
|
|
308
313
|
runner rather than by `evaluate_agentic_*`, so a direct library caller sees `langfuse_s: 0.0` even though its
|
|
309
314
|
linking ran. Pass `TAVERN_E2E_SKIP_TRACE_LINK=1` to opt out of linking altogether.
|
|
310
315
|
|
|
316
|
+
### Latency: single-shot and multi-turn
|
|
317
|
+
|
|
318
|
+
`avg_latency_s` is one number over a mixed population. The report instead keeps two kinds of item apart, by
|
|
319
|
+
their eval kind, and states latency for each:
|
|
320
|
+
|
|
321
|
+
- **Single-shot** — kinds that send exactly one chat message per run: every non-agentic kind, plus
|
|
322
|
+
`agentic_general_question`, `agentic_guardrail` and `agentic_search`.
|
|
323
|
+
- **Multi-turn** — kinds that drive a simulated user or fixture turns: `agentic_visualization` (and
|
|
324
|
+
`vis_agentic`), `agentic_metric_skill`, `agentic_alert_skill`, `agentic_dashboard_skill`,
|
|
325
|
+
`agentic_document_skill`, `agentic_kda_skill`, `agentic_anomaly_detection`, `agentic_what_if` and
|
|
326
|
+
`agentic_conversation`.
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
"summary": {
|
|
330
|
+
"by_mode": {
|
|
331
|
+
"single_shot": {"total": 12, "passed": 10,
|
|
332
|
+
"traces_s": {"n": 24, "mean": 5.39, "trimmed_mean": 5.36, "min": 3.88, "p50": 5.33, "p70": 6.23, "p90": 6.82, "p99": 7.18, "max": 7.22}},
|
|
333
|
+
"multi_turn": {"total": 6, "passed": 4,
|
|
334
|
+
"traces_s": {"n": 45, "mean": 6.91, "trimmed_mean": 6.84, "min": 2.78, "p50": 6.52, "p70": 8.03, "p90": 10.79, "p99": 13.57, "max": 14.25},
|
|
335
|
+
"runs_s": {"n": 15, "mean": 23.55, "trimmed_mean": 23.31, "min": 12.64, "p50": 21.23, "p70": 28.52, "p90": 33.02, "p99": 36.94, "max": 37.51}}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`by_mode` is in each run's `summary` and in its `comparison` entry. `total` and `passed` count the evaluated
|
|
341
|
+
items of that mode (skipped ones belong to neither). Every distribution is `null` when nothing was timed.
|
|
342
|
+
Each item also states its `mode`, its `trace_latencies_s` and its `run_latencies_s`.
|
|
343
|
+
|
|
344
|
+
- **Per trace is the main figure.** gen-ai starts one trace per chat message, so a trace is one chat turn.
|
|
345
|
+
Its latency is that turn's response time, from sending the message to the end of its stream
|
|
346
|
+
(`turn_wall_clock_sec`), recorded where the chat client completes the turn. It holds none of the LLM judge,
|
|
347
|
+
the simulated user or conversation create/delete. `dashboard_summary` is timed the same way around its one
|
|
348
|
+
request. A turn cut off by its turn or item deadline, or one that ended without a final answer (the agent's
|
|
349
|
+
iteration or token limit, a content filter), still counts, with the time it ran for: those are the slowest
|
|
350
|
+
turns, and the run that scored them is in the per-run figures too. A turn that ended in any other error (an
|
|
351
|
+
error event, or a 503 that outlasted its retries) does not: the time is that of a failed request, not a
|
|
352
|
+
response, and would only add a fast outlier. On the single-shot path such a turn ends its item as an error, so
|
|
353
|
+
it has no per-run figure either. An item that errored outside its evaluator's own failure reporting
|
|
354
|
+
contributes neither.
|
|
355
|
+
- **Single-shot** runs are exactly one trace, so `traces_s` is also the latency of a run.
|
|
356
|
+
- **Multi-turn** runs span several traces, so they get both views: `traces_s` over every turn of every
|
|
357
|
+
conversation (comparable with single-shot), and `runs_s`, the wall clock of each whole conversation from just
|
|
358
|
+
before its first message to the end of its scoring — every turn, simulated-user reply and judge call, but not
|
|
359
|
+
conversation create/delete. `agentic_conversation` drives its fixture once and contributes one run per item.
|
|
360
|
+
The two single-shot and multi-turn populations are never mixed.
|
|
361
|
+
- **Percentiles** use linear interpolation between closest ranks (NumPy's default), so `p50` is the median and
|
|
362
|
+
the report carries no separate median field. On a small sample the high percentiles sit at or near the
|
|
363
|
+
maximum: P99 over fewer than 100 samples is effectively the slowest one, which is why `n` always travels with it.
|
|
364
|
+
- **Trimmed mean** (`trimmed_mean`) is the average of the middle 80%: the fastest 10% and the slowest 10% of the
|
|
365
|
+
samples are dropped from each end (`n * 10 // 100` of them, so 25 samples drop two each) before averaging. It is
|
|
366
|
+
not the median, which is one middle value (`p50`). It is `null` below 10 samples, where there is nothing to
|
|
367
|
+
trim and the figure would only repeat `mean`.
|
|
368
|
+
- **Older reports** have no `by_mode`. The HTML report says so and nothing is backfilled.
|
|
369
|
+
|
|
370
|
+
`avg_latency_s` and the other existing fields are unchanged. The console prints each run's pass counts per
|
|
371
|
+
mode and one `Latency, <population> (n=...)` line per distribution beneath its summary line, and Trace
|
|
372
|
+
P50/P90/P99 columns (single-shot) in the multi-model comparison.
|
|
373
|
+
|
|
311
374
|
---
|
|
312
375
|
|
|
313
376
|
## `gd-eval report`
|
|
314
377
|
|
|
315
378
|
Turns JSON report(s) into one HTML file you can actually navigate. No server, no
|
|
316
379
|
credentials, no external assets — it opens over `file://`, attaches to a Jira issue and
|
|
317
|
-
survives a Slack thread.
|
|
380
|
+
survives a Slack thread. It follows GoodData's product look (colours, type, status chips). The faces are Inter, with
|
|
381
|
+
Funnel Display for headings, named but never loaded, so a viewer without them gets the system sans and the file stays
|
|
382
|
+
self-contained.
|
|
318
383
|
|
|
319
384
|
```bash
|
|
320
385
|
# one run
|
|
@@ -331,27 +396,134 @@ gd-eval report results.json -o customer.html --redact
|
|
|
331
396
|
|---|---|
|
|
332
397
|
| `-o, --out PATH` | Where to write the HTML. Required. |
|
|
333
398
|
| `--title TEXT` | Title shown in the report header. |
|
|
334
|
-
| `--redact` | Drop conversation/response ids and raw reasoning, and rename models to `Model A`, `Model B`, … Pass rate, per-item results, questions and latency survive. |
|
|
335
|
-
|
|
336
|
-
The report is a *view* over the JSON
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
399
|
+
| `--redact` | Drop conversation/response ids and raw reasoning, and rename models to `Model A`, `Model B`, … A model keeps its letter across all its runs, which are then numbered (`Run 1 · Model A`, `Run 2 · Model A`). Pass rate, per-item results, questions and latency survive. |
|
|
400
|
+
|
|
401
|
+
The report is a *view* over the JSON. Unfiltered, every figure on it is read from the report, so it cannot
|
|
402
|
+
disagree with the console or the JSON. Narrow the selection with a filter and the figures are recomputed in the
|
|
403
|
+
page from the selected items' own records, by the same rules gd-eval uses. The page reads top to bottom as a
|
|
404
|
+
document, in plain words: a *test case* is one prompt given to the assistant, an *attempt* is one go at it, and the
|
|
405
|
+
two formats are *single-question* tests (one answer, no follow-up) and *conversation* tests (a back-and-forth with a
|
|
406
|
+
simulated user), which are `single_shot` and `multi_turn` in the JSON.
|
|
407
|
+
|
|
408
|
+
- **A masthead** with the report's title (`--title`, or "Run history", "Run comparison", "Model comparison" or
|
|
409
|
+
"Run results" plus the model and workspace), when it was generated in the reader's own time zone, and a **Run setup** tooltip listing what
|
|
410
|
+
is the same in every run (model, provider, workspace, dataset, test cases, attempts per test case, pass rule) and
|
|
411
|
+
what differs. Results from different setups are not directly comparable, so a caution icon on a latency row and the
|
|
412
|
+
*New* / *Removed* marks in the test case table say where two runs stopped being alike.
|
|
413
|
+
- **View by** — a dropdown above the filters that lays the same report out two ways. **Date** is the layout the
|
|
414
|
+
rest of this list describes: a column per run, oldest to latest by start time, with change lines, sparklines and the latency line
|
|
415
|
+
chart. **Model** compares language models instead: a column per model, ranked best first by the console's winner
|
|
416
|
+
rule (pass rate, then quality, then the quicker average), with no time axis. Where a model has several runs its
|
|
417
|
+
latest stands for it, so each model is read at its most recent state. The Run filter becomes a Model filter, the hint
|
|
418
|
+
beside the dropdown says what is being compared (and names the dates when the compared runs differ in date), and the
|
|
419
|
+
Run setup tooltip does not flag the model itself as a difference. Every report opens on Date, including one that
|
|
420
|
+
compares several models (`gd-eval run --model a --model b --html`); choose Model to compare them. A single run has
|
|
421
|
+
nothing to switch between, so the dropdown is hidden. In the Model view the page reads, top to bottom:
|
|
422
|
+
- **Results** — one row per model, best pass rate first, with a bar of how its evaluated test cases ended (passed
|
|
423
|
+
green, failed red, could not be graded orange), so the green part is the pass rate beside it, and the **Median
|
|
424
|
+
latency** of one attempt (half the attempts were faster, half slower). A list under the table says what each term means. The counts and the
|
|
425
|
+
caution icon (a model evaluated on different test cases than the one with the most) are the date view's Outcomes
|
|
426
|
+
table. There are no headline cards by model: the table is the first thing on the page.
|
|
427
|
+
- **Quality against speed** — each model as a point: the share of test cases it passed against the median time of
|
|
428
|
+
one attempt, the same pass rate as the Results table and the same median as the latency tables. Up and to the left is better. Time runs from zero;
|
|
429
|
+
the pass rate uses the range the models span. It needs two models with timings.
|
|
430
|
+
- **By category** — a heat map of the pass rate per category and model, the best model in each row marked, the
|
|
431
|
+
categories where the models differ most first. Every cell prints its share and its counts, so the shade is never the
|
|
432
|
+
only signal. This is where a model that wins overall but loses on one category shows.
|
|
433
|
+
- **Consistency across attempts** — per model, the share of test cases that passed on at least one attempt (pass@K,
|
|
434
|
+
the light bar) beside the share that passed on every attempt (pass^K, the dark bar), and how many passed only some.
|
|
435
|
+
A long gap is a model that can do the task but not reliably. It appears only for a report that recorded
|
|
436
|
+
`pass_power_k` and attempted a test case more than once.
|
|
437
|
+
- **Latency sections** — the same four percentiles, one row per model, quickest first, with a bar per figure drawn
|
|
438
|
+
from zero beside the numbers. The quickest and slowest model in each column are marked as in the date view.
|
|
439
|
+
- **Test cases** — one result column per model in ranked order, a response-time column per model, and an **Across
|
|
440
|
+
models** column in place of *Since previous run*: *Mixed* (some models passed and some did not; hover to see who),
|
|
441
|
+
*All pass* or *All fail*. A skipped or ungraded test case is left out of the verdict. The table opens on the
|
|
442
|
+
*Mixed* ones, the test cases where the models differ.
|
|
443
|
+
- **Filters in two tiers** — the **Filter the report** bar at the top (category, format, result and run) decides what
|
|
444
|
+
the whole page covers: the Results cards, the outcomes table, the latency tables and the test case table all follow it, so the runs stay
|
|
445
|
+
comparable. The Test cases section repeats those four and adds the search box and the expression; they narrow the
|
|
446
|
+
table only, inside whatever the top bar allows, and only offer what the top bar leaves. Category, result and run are
|
|
447
|
+
attribute filters like the product's: a search box, an All row, the values with an **Only** shortcut and a count,
|
|
448
|
+
then Cancel and Apply. Changes are staged in the popup and take effect on Apply; closing it any other way discards
|
|
449
|
+
them. Nothing ticked means all. The run filter chooses which runs are shown; a result matches a test case when any of
|
|
450
|
+
the runs in play has it (so "Failed" over previous and latest finds every test case that broke in either), and the
|
|
451
|
+
expression is evaluated once per run in play with the same any-run rule. A filtered page states how many test cases
|
|
452
|
+
it covers, and each tier has its own Reset.
|
|
453
|
+
- **Results** — three headline cards for the latest run: test cases passed (of those evaluated: skipped ones are not
|
|
454
|
+
counted, as in the console's percentage), the average quality score and the median time per attempt. That last one
|
|
455
|
+
pools every format on the page and counts whole attempts, grading included, so it matches neither per-response latency table
|
|
456
|
+
below it; its tooltip says which formats it covers. With an earlier run each card carries one change line in words, "down from 86% on Sep 28", rather
|
|
457
|
+
than a signed number. The line is green when that direction is good and red when it is not, with a chevron and
|
|
458
|
+
screen-reader text (*Better* / *Worse*) so colour is not the only signal. From three runs each card also has a
|
|
459
|
+
sparkline of every run: hover a point for its value, click it to compare the latest run with that one instead of the
|
|
460
|
+
previous. The dot being compared with is drawn white, and a key under the cards says so (latest run, compared with,
|
|
461
|
+
other runs) and that a click on any other dot moves the comparison; it shows only when a sparkline does. A pick that
|
|
462
|
+
a filter has removed, or that has become the latest run, falls back to the previous run. The
|
|
463
|
+
pass-rate card links to the test case table to see *which* ones changed; the split by format lives in that table's
|
|
464
|
+
filters.
|
|
465
|
+
- **Outcomes** — how every test case ended, as a table with one row per run, newest first, in the same layout as the
|
|
466
|
+
latency tables. A run is identified by three columns: the **Run date**, the day and time it started (`summary.started_at`,
|
|
467
|
+
shown in the reader's own time zone, the UTC stamp on hover; a report written before it existed shows the date its file is
|
|
468
|
+
named for, without a time), and the **Run name**, the name the run was given, as given, so a run named `2026-09-28` reads
|
|
469
|
+
`2026-09-28` here although the charts style it as "Sep 28", and the **Model** it evaluated, its provider on hover. Then **Passed**, **Failed**, **Could not be graded** (an error left it without a verdict) and
|
|
470
|
+
**Skipped** (not run), then the **Pass rate**. The four are the values of the Result filter, and the pass rate is
|
|
471
|
+
the card's: passed out of the evaluated test cases, skipped ones left out, as in the console. The highest pass rate
|
|
472
|
+
is marked green with an up chevron and the lowest red with a down chevron, and the latest run is marked in lime. A
|
|
473
|
+
caution icon flags a run evaluated over a different number of test cases than the one before, because its pass
|
|
474
|
+
rate is then not a like-for-like change. The counts are read from the run's `summary` (`passed`, `failed`,
|
|
475
|
+
`errored`, `skipped`); a filtered view recounts them from the selected test cases, and a report written before
|
|
476
|
+
`summary.failed` existed shows what the other three leave.
|
|
477
|
+
- **Latency sections** — **Single-question tests · response time** first, then **Conversation tests**, which shows
|
|
478
|
+
both per response (each assistant reply, one gen-ai trace) and per conversation (start to finish, with the
|
|
479
|
+
simulated user and the judge). Each opens with a **line chart** of the median (P50), P70, P90 and P99 across the
|
|
480
|
+
runs, oldest on the left and the latest on the right, drawn from zero so line heights compare honestly; the lines
|
|
481
|
+
share one blue that darkens towards the tail. Hover or focus a run for its crosshair and every figure, in a tooltip
|
|
482
|
+
that says how many responses it rests on. A line needs two runs with timings, so with one run the chart is left
|
|
483
|
+
out. End labels name each line and give its latest value, and are dropped when two would touch. Under the chart a
|
|
484
|
+
table has one row per run, newest first, identified by the same **Run date**, **Run name** and **Model** columns as the outcomes table, with the same four figures. The quickest run in each column is marked
|
|
485
|
+
green and the slowest red, each with a chevron (down for least time, up for most) so colour is not the only
|
|
486
|
+
signal, and the latest run is marked in lime. A caution icon flags a run timed on a different number of samples
|
|
487
|
+
than the one before. Unfiltered they are read from `by_mode`; a filtered view recomputes them in the page by the
|
|
488
|
+
same rules. The fastest and slowest single response, the average and the trimmed average stay in the JSON (P99
|
|
489
|
+
over a few dozen samples sits close to the slowest one, which is why `n` travels with it in the tooltip and
|
|
490
|
+
the cell hint). Seconds read with one decimal and no redundant ".0" (`3s`, `4.6s`).
|
|
491
|
+
- **A test case table**, newest first: the latest run's result, a **Since previous run** column, then the earlier runs
|
|
492
|
+
going back in time (click any result to open the drawer on that run). Each run's column is headed by its run date over
|
|
493
|
+
its run name, one to a line (hover the heading for the full run label); by model the columns are headed by the model. The change column reads *Regressed* (passed in
|
|
494
|
+
the previous run, not in the latest), *Fixed*, *Flaky* (flipped between pass and fail more than once), *New*
|
|
495
|
+
(in no earlier run) or *Removed* (in an earlier run, gone from the latest). A test case skipped in the latest run
|
|
496
|
+
has no label, since the change would describe earlier runs. With several runs the table opens on the regressions. Two time columns
|
|
497
|
+
keep the populations apart: **Response time** is the median of the test case's assistant responses, and **Time per
|
|
498
|
+
attempt** is the mean wall-clock of a whole attempt (the responses plus the judge and, for conversations, the
|
|
499
|
+
simulated user). With more than two runs shown they follow the latest run only. Hover a value to see every sample
|
|
500
|
+
behind it; the drawer labels its two latency chips the same way. A report written before per-trace timing has no
|
|
501
|
+
response figure and shows `avg_latency_s` as the attempt figure.
|
|
502
|
+
- **An expression filter**, folded under *Advanced: filter by a check expression* in the Test cases section, for
|
|
503
|
+
cross-cutting questions the fixed filters can't anticipate, e.g. `d.filter_ranking_score === false` or
|
|
343
504
|
`d.expected_metric_uris.length > 1 && !d.metrics_correct`. Available variables:
|
|
344
|
-
`d` (the
|
|
345
|
-
run), `q` (question), `kind
|
|
346
|
-
- **The conversation**, when the
|
|
505
|
+
`d` (the checks of the run being read, its `detail`), `it` (that run's result), `i` (the row, `i.per[label]` for any
|
|
506
|
+
run), `q` (question), `kind` (category id).
|
|
507
|
+
- **The conversation**, when the test case ran the agentic multi-turn path — every turn in
|
|
347
508
|
order, with the simulated user marked apart from a real question, so you can see
|
|
348
509
|
whether the agent got there or was handed the answer.
|
|
349
|
-
- **A per-
|
|
350
|
-
|
|
510
|
+
- **A per-test-case drawer** — checks as pass/fail chips, expected vs actual side by side,
|
|
511
|
+
the latency timeline, conversation/response ids.
|
|
351
512
|
- **A latency timeline** from `detail.latency_breakdown`, in execution order, one bar per
|
|
352
|
-
step.
|
|
353
|
-
reasoning step was summarised from, or a tool call's arguments and result from
|
|
354
|
-
`detail.tool_calls`.
|
|
513
|
+
step. Every step has a chevron that opens its full record in place, joined by `index`: the
|
|
514
|
+
paragraph a reasoning step was summarised from, or a tool call's arguments and result from
|
|
515
|
+
`detail.tool_calls`. A reasoning paragraph's markdown `**title**` is shown as bold text,
|
|
516
|
+
not as asterisks. Any number of steps can be open at once, and the chevron above the list
|
|
517
|
+
opens or closes all of them. The reasoning therefore lives on the timeline; a separate
|
|
518
|
+
Reasoning section appears only for paragraphs no step points at (or when a run recorded no
|
|
519
|
+
timeline).
|
|
520
|
+
- **A print stylesheet** that drops the filters and the drawer, so "Save as PDF" gives a clean page.
|
|
521
|
+
|
|
522
|
+
A report that merges several files names each run after its file, and a file named for its date (`2026-09-28.json`)
|
|
523
|
+
reads as "Sep 28". A model is read from the run itself (`model` and the provider), never from the file name, so files
|
|
524
|
+
named for their model work as well as files named for their date. In the Date view the page treats the runs as one
|
|
525
|
+
model followed over time; when a report holds several models it adds the model to each run's name and points at
|
|
526
|
+
**Model** (see above) for the comparison.
|
|
355
527
|
|
|
356
528
|
`--redact` additionally drops `transcript` and `tool_calls`: the exchange shows that the
|
|
357
529
|
simulated user is primed with the expected output, and a tool result carries
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# (C) 2026 GoodData Corporation
|
|
2
2
|
[project]
|
|
3
3
|
name = "gooddata-eval"
|
|
4
|
-
version = "1.
|
|
4
|
+
version = "1.78.0"
|
|
5
5
|
description = "Evaluate the GoodData AI agent against your own questions and models."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
keywords = ["gooddata", "ai", "evaluation", "llm", "analytics", "cli"]
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"gooddata-sdk~=1.
|
|
14
|
+
"gooddata-sdk~=1.78.0",
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"jsonpatch>=1.33,<2.0",
|
|
17
17
|
"orjson>=3.9.15,<4.0.0",
|