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.
Files changed (146) hide show
  1. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/AGENTS.md +5 -2
  2. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/PKG-INFO +192 -20
  3. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/README.md +190 -18
  4. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/pyproject.toml +2 -2
  5. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/cli/agentic_runner.py +30 -0
  6. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/cli/main.py +4 -0
  7. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/alert_skill.py +131 -17
  8. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/anomaly_detection.py +7 -0
  9. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/conversation.py +14 -0
  10. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/dashboard_skill.py +6 -0
  11. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/document_skill.py +7 -0
  12. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/general_question.py +11 -2
  13. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/guardrail.py +11 -2
  14. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/kda_skill.py +7 -0
  15. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/metric_skill.py +7 -0
  16. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/search_tool.py +9 -0
  17. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/visualization.py +8 -0
  18. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/what_if.py +7 -0
  19. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/chat/sse_client.py +15 -0
  20. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_llm_judge.py +1 -0
  21. gooddata_eval-1.78.0/src/gooddata_eval/core/interaction.py +57 -0
  22. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/models.py +6 -0
  23. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/reporting/console.py +52 -0
  24. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/reporting/html_report.py +40 -5
  25. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/reporting/json_report.py +19 -0
  26. gooddata_eval-1.78.0/src/gooddata_eval/core/reporting/report_template.html +1960 -0
  27. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/runner.py +67 -1
  28. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/summary/http_client.py +11 -1
  29. gooddata_eval-1.78.0/src/gooddata_eval/core/timing.py +184 -0
  30. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_alert_skill.py +212 -1
  31. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_anomaly_detection.py +3 -0
  32. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_conversation.py +3 -0
  33. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_dashboard_skill.py +3 -0
  34. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_document_skill.py +5 -1
  35. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_general_question.py +8 -0
  36. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_guardrail.py +6 -0
  37. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_kda_skill.py +3 -0
  38. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_metric_skill.py +12 -4
  39. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_runner.py +170 -1
  40. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_search_tool.py +5 -0
  41. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_visualization.py +5 -0
  42. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_what_if.py +3 -0
  43. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_cli.py +55 -0
  44. gooddata_eval-1.78.0/tests/test_html_report.py +1364 -0
  45. gooddata_eval-1.78.0/tests/test_interaction.py +35 -0
  46. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_reporting.py +159 -0
  47. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_runner.py +200 -1
  48. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_sse_client.py +81 -0
  49. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_summary_client.py +6 -0
  50. gooddata_eval-1.78.0/tests/test_timing.py +270 -0
  51. gooddata_eval-1.77.1.dev1/src/gooddata_eval/core/reporting/report_template.html +0 -452
  52. gooddata_eval-1.77.1.dev1/src/gooddata_eval/core/timing.py +0 -74
  53. gooddata_eval-1.77.1.dev1/tests/test_html_report.py +0 -202
  54. gooddata_eval-1.77.1.dev1/tests/test_timing.py +0 -93
  55. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/.gitignore +0 -0
  56. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/CLAUDE.md +0 -0
  57. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/LICENSE.txt +0 -0
  58. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/Makefile +0 -0
  59. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/scripts/verify_guardrail_refusal_criteria.py +0 -0
  60. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/__init__.py +0 -0
  61. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/_version.py +0 -0
  62. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/cli/__init__.py +0 -0
  63. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/__init__.py +0 -0
  64. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/_output.py +0 -0
  65. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/__init__.py +0 -0
  66. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
  67. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
  68. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
  69. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_gate.py +0 -0
  70. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
  71. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
  72. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
  73. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/chat/__init__.py +0 -0
  74. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/chat/render.py +0 -0
  75. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/config.py +0 -0
  76. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/connection.py +0 -0
  77. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  78. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
  79. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
  80. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/dataset/local.py +0 -0
  81. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
  82. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  83. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
  84. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
  85. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  86. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
  87. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/base.py +0 -0
  88. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  89. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  90. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  91. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
  92. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/summary.py +0 -0
  93. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
  94. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/granularity.py +0 -0
  95. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  96. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/_env.py +0 -0
  97. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/client.py +0 -0
  98. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
  99. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
  100. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/observations.py +0 -0
  101. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
  102. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  103. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  104. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/scoring.py +0 -0
  105. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/summary/__init__.py +0 -0
  106. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/src/gooddata_eval/core/workspace.py +0 -0
  107. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/__init__.py +0 -0
  108. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/_fake_langfuse.py +0 -0
  109. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/conftest.py +0 -0
  110. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  111. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  112. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/fixtures/sse_visualization_stream.txt +0 -0
  113. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_gate.py +0 -0
  114. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_join_trace.py +0 -0
  115. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_langfuse_trace.py +0 -0
  116. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_observe_experiment.py +0 -0
  117. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_agentic_run_context.py +0 -0
  118. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_alert_skill_evaluator.py +0 -0
  119. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_chat_render.py +0 -0
  120. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_connection.py +0 -0
  121. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_deep_subset.py +0 -0
  122. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_fake_langfuse.py +0 -0
  123. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_from_insights.py +0 -0
  124. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_guardrail_criteria.py +0 -0
  125. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_client.py +0 -0
  126. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_e2e_fake_server.py +0 -0
  127. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_env.py +0 -0
  128. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_experiment.py +0 -0
  129. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_item_scope.py +0 -0
  130. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_observations.py +0 -0
  131. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_otlp.py +0 -0
  132. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_sink.py +0 -0
  133. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_langfuse_source.py +0 -0
  134. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_llm_judge.py +0 -0
  135. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_local_loader.py +0 -0
  136. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_maql_normalize.py +0 -0
  137. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_metric_skill_evaluator.py +0 -0
  138. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_models.py +0 -0
  139. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_scoring.py +0 -0
  140. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_search_tool_evaluator.py +0 -0
  141. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_summary_evaluator.py +0 -0
  142. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_text_evaluators.py +0 -0
  143. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_trace_linker.py +0 -0
  144. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_visualization_evaluator.py +0 -0
  145. {gooddata_eval-1.77.1.dev1 → gooddata_eval-1.78.0}/tests/test_workspace.py +0 -0
  146. {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`. Test file naming follows the capability, but
172
- naming a test file correctly registers nothing.
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.77.1.dev1
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.77.1.dev1
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 — it computes no numbers of its own. It gives you:
366
-
367
- - **Run cards and a comparison table** — pass rate, quality, latency per run.
368
- - **An item table** with a pass/fail column per run, so a model or run-over-run
369
- regression is one glance rather than a hand-assembled spreadsheet.
370
- - **An expression filter** for cross-cutting questions the fixed filters can't
371
- anticipate, e.g. `d.filter_ranking_score === false` or
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 focused run's `detail`), `it` (its item), `i` (the row, `i.per[label]` for any
374
- run), `q` (question), `kind`.
375
- - **The conversation**, when the item ran the agentic multi-turn path — every turn in
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-item drawer** — checks as pass/fail chips, expected vs actual side by side,
379
- full reasoning, conversation/response ids.
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. Clicking a step expands its full record, joined by `index`: the paragraph a
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 — it computes no numbers of its own. It gives you:
337
-
338
- - **Run cards and a comparison table** — pass rate, quality, latency per run.
339
- - **An item table** with a pass/fail column per run, so a model or run-over-run
340
- regression is one glance rather than a hand-assembled spreadsheet.
341
- - **An expression filter** for cross-cutting questions the fixed filters can't
342
- anticipate, e.g. `d.filter_ranking_score === false` or
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 focused run's `detail`), `it` (its item), `i` (the row, `i.per[label]` for any
345
- run), `q` (question), `kind`.
346
- - **The conversation**, when the item ran the agentic multi-turn path — every turn in
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-item drawer** — checks as pass/fail chips, expected vs actual side by side,
350
- full reasoning, conversation/response ids.
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. Clicking a step expands its full record, joined by `index`: the paragraph a
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.77.1.dev1"
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.77.1.dev1",
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",