gooddata-eval 1.77.1.dev2__tar.gz → 1.78.1.dev1__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 (158) hide show
  1. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/AGENTS.md +19 -11
  2. gooddata_eval-1.78.1.dev1/Makefile +6 -0
  3. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/PKG-INFO +256 -24
  4. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/README.md +254 -22
  5. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/pyproject.toml +2 -2
  6. gooddata_eval-1.78.1.dev1/scripts/calibrate_takeaway_judge.py +75 -0
  7. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/cli/agentic_runner.py +30 -0
  8. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/cli/main.py +21 -11
  9. gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/_retry.py +71 -0
  10. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_conversation_context.py +59 -10
  11. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/alert_skill.py +7 -0
  12. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/anomaly_detection.py +7 -0
  13. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/conversation.py +14 -0
  14. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/dashboard_skill.py +6 -0
  15. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/document_skill.py +7 -0
  16. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/general_question.py +11 -2
  17. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/guardrail.py +11 -2
  18. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/kda_skill.py +7 -0
  19. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/metric_skill.py +7 -0
  20. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/search_tool.py +9 -0
  21. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/visualization.py +8 -0
  22. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/what_if.py +7 -0
  23. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/chat/sse_client.py +28 -59
  24. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/dataset/langfuse_source.py +29 -2
  25. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/__init__.py +5 -2
  26. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_llm_judge.py +1 -0
  27. gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/evaluators/_rubric.py +94 -0
  28. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/summary.py +12 -64
  29. gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/evaluators/takeaway.py +245 -0
  30. gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/interaction.py +57 -0
  31. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/models.py +47 -0
  32. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/console.py +52 -0
  33. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/html_report.py +40 -5
  34. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/json_report.py +19 -0
  35. gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/reporting/report_template.html +1960 -0
  36. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/runner.py +67 -1
  37. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/summary/http_client.py +11 -1
  38. gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/takeaway/evaluate.py +275 -0
  39. gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/takeaway/http_client.py +104 -0
  40. gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/timing.py +184 -0
  41. gooddata_eval-1.78.1.dev1/tests/__init__.py +1 -0
  42. gooddata_eval-1.78.1.dev1/tests/fixtures/takeaway_calibration.json +68 -0
  43. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_alert_skill.py +3 -0
  44. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_anomaly_detection.py +3 -0
  45. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_conversation.py +118 -0
  46. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_dashboard_skill.py +3 -0
  47. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_document_skill.py +5 -1
  48. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_general_question.py +8 -0
  49. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_guardrail.py +6 -0
  50. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_kda_skill.py +3 -0
  51. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_metric_skill.py +12 -4
  52. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_runner.py +170 -1
  53. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_search_tool.py +5 -0
  54. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_visualization.py +5 -0
  55. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_what_if.py +3 -0
  56. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_cli.py +106 -0
  57. gooddata_eval-1.78.1.dev1/tests/test_html_report.py +1364 -0
  58. gooddata_eval-1.78.1.dev1/tests/test_interaction.py +35 -0
  59. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_source.py +96 -0
  60. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_models.py +50 -0
  61. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_reporting.py +159 -0
  62. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_runner.py +201 -1
  63. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_sse_client.py +87 -0
  64. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_summary_client.py +6 -0
  65. gooddata_eval-1.78.1.dev1/tests/test_takeaway_client.py +257 -0
  66. gooddata_eval-1.78.1.dev1/tests/test_takeaway_evaluate.py +349 -0
  67. gooddata_eval-1.78.1.dev1/tests/test_takeaway_evaluator.py +425 -0
  68. gooddata_eval-1.78.1.dev1/tests/test_timing.py +270 -0
  69. gooddata_eval-1.77.1.dev2/Makefile +0 -2
  70. gooddata_eval-1.77.1.dev2/src/gooddata_eval/core/reporting/report_template.html +0 -452
  71. gooddata_eval-1.77.1.dev2/src/gooddata_eval/core/timing.py +0 -74
  72. gooddata_eval-1.77.1.dev2/tests/test_html_report.py +0 -202
  73. gooddata_eval-1.77.1.dev2/tests/test_timing.py +0 -93
  74. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/.gitignore +0 -0
  75. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/CLAUDE.md +0 -0
  76. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/LICENSE.txt +0 -0
  77. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/scripts/verify_guardrail_refusal_criteria.py +0 -0
  78. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/__init__.py +0 -0
  79. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/_version.py +0 -0
  80. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/cli/__init__.py +0 -0
  81. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/__init__.py +0 -0
  82. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/_output.py +0 -0
  83. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/__init__.py +0 -0
  84. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
  85. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
  86. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_gate.py +0 -0
  87. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
  88. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
  89. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
  90. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/chat/__init__.py +0 -0
  91. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/chat/render.py +0 -0
  92. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/config.py +0 -0
  93. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/connection.py +0 -0
  94. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  95. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
  96. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/dataset/local.py +0 -0
  97. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  98. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
  99. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
  100. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  101. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
  102. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/base.py +0 -0
  103. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  104. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  105. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  106. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
  107. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
  108. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/granularity.py +0 -0
  109. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  110. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/_env.py +0 -0
  111. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/client.py +0 -0
  112. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
  113. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
  114. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/observations.py +0 -0
  115. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
  116. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  117. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  118. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/scoring.py +0 -0
  119. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/summary/__init__.py +0 -0
  120. {gooddata_eval-1.77.1.dev2/tests → gooddata_eval-1.78.1.dev1/src/gooddata_eval/core/takeaway}/__init__.py +0 -0
  121. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/src/gooddata_eval/core/workspace.py +0 -0
  122. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/_fake_langfuse.py +0 -0
  123. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/conftest.py +0 -0
  124. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  125. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  126. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/fixtures/sse_visualization_stream.txt +0 -0
  127. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_gate.py +0 -0
  128. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_join_trace.py +0 -0
  129. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_langfuse_trace.py +0 -0
  130. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_observe_experiment.py +0 -0
  131. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_agentic_run_context.py +0 -0
  132. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_alert_skill_evaluator.py +0 -0
  133. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_chat_render.py +0 -0
  134. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_connection.py +0 -0
  135. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_deep_subset.py +0 -0
  136. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_fake_langfuse.py +0 -0
  137. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_from_insights.py +0 -0
  138. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_guardrail_criteria.py +0 -0
  139. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_client.py +0 -0
  140. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_e2e_fake_server.py +0 -0
  141. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_env.py +0 -0
  142. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_experiment.py +0 -0
  143. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_item_scope.py +0 -0
  144. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_observations.py +0 -0
  145. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_otlp.py +0 -0
  146. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_langfuse_sink.py +0 -0
  147. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_llm_judge.py +0 -0
  148. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_local_loader.py +0 -0
  149. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_maql_normalize.py +0 -0
  150. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_metric_skill_evaluator.py +0 -0
  151. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_scoring.py +0 -0
  152. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_search_tool_evaluator.py +0 -0
  153. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_summary_evaluator.py +0 -0
  154. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_text_evaluators.py +0 -0
  155. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_trace_linker.py +0 -0
  156. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_visualization_evaluator.py +0 -0
  157. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tests/test_workspace.py +0 -0
  158. {gooddata_eval-1.77.1.dev2 → gooddata_eval-1.78.1.dev1}/tox.ini +0 -0
@@ -27,6 +27,7 @@ experiment. The newest and most actively developed package in the repo.
27
27
  | `core/agentic/` | multi-turn agentic evaluation per capability, **plus** all Langfuse trace polling and linking (`_langfuse.py`, `_trace_linker.py`) |
28
28
  | `core/chat/` | SSE client for the agent's streaming chat endpoint |
29
29
  | `core/summary/` | HTTP client for the dedicated dashboard-summary endpoint — a single-shot chat backend, not reporting |
30
+ | `core/takeaway/` | HTTP client for the AI Publisher takeaway endpoint — a single-shot backend like `core/summary/`, keeping the whole response on `ChatResult.raw_response` — plus `evaluate_document_takeaway`, the K-run, gate and Langfuse entry point the Tavern shim calls |
30
31
  | `core/dataset/` | dataset format, loading, and `from_insights.py` — dataset generation from a workspace's real insights |
31
32
  | `core/evaluators/` | single-shot evaluators and their registry |
32
33
  | `core/langfuse/` | the whole Langfuse v4 client: `_env` (base URL + credentials), `otlp` (OTLP/JSON encoding), `experiment` (root-span construction, score targets), `item_scope` (an item's experiment scope sent to gen-ai as baggage, and which conversations joined), `observations` (trace reads), `client` (httpx calls), `sink` (single-shot results as experiments) |
@@ -59,7 +60,8 @@ implementation, sometimes under different `test_kind` strings (`search_tool` vs
59
60
  `expected_output: Any`. `expected_output` is deliberately untyped — each evaluator parses
60
61
  its own shape. `test_kind` on the item is what labels the result, not the evaluator class,
61
62
  which is why `knowledge_question` can reuse `GeneralQuestionEvaluator` verbatim.
62
- `dashboard_summary` items additionally need `summary_input`.
63
+ `dashboard_summary` items additionally need `summary_input`, `document_takeaway` items
64
+ `takeaway_input`.
63
65
 
64
66
  ## Running the pipeline
65
67
 
@@ -155,8 +157,8 @@ Run this before guessing a `--model` string.
155
157
  | `GD_EVAL_JUDGE_MODEL` | judge model, same as `--judge-model` |
156
158
  | `GD_EVAL_AGENT_ID` | which agent to drive, same as `--agent-id` |
157
159
  | `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` | `--langfuse`, `--langfuse-dataset` |
158
- | `GOODDATA_EVAL_CHAT_*` | SSE retry, backoff and timeout knobs |
159
- | `GOODDATA_EVAL_TRACE_LABELS` | `key=value,...` labels stamped on every observation of a chat conversation via W3C baggage; `model_version` also sets the observation `version`, so set it only for a one-model run: every `--model` in one process gets the same labels. Keys must match `[A-Za-z0-9_-]+`; others are skipped with a warning. Dashboard-summary runs are not labelled |
160
+ | `GOODDATA_EVAL_CHAT_*` | retry and backoff knobs shared by the chat and takeaway clients (`core/_retry.py`), and the SSE turn and item timeouts |
161
+ | `GOODDATA_EVAL_TRACE_LABELS` | `key=value,...` labels stamped on every observation of a chat conversation via W3C baggage; `model_version` also sets the observation `version`, so set it only for a one-model run: every `--model` in one process gets the same labels. Keys must match `[A-Za-z0-9_-]+`; others are skipped with a warning. Dashboard-summary and document-takeaway runs are not labelled |
160
162
  | `GOODDATA_EVAL_JOIN_GENAI_TRACE` | on unless set to `0`, `false`, `no`, `off` or empty. An agentic item's chat turns ask gen-ai to open them under gd-eval's experiment root, so each item is one Langfuse trace with its scores written once. A gen-ai that drops caller baggage keeps today's separate traces; see the gotcha below |
161
163
  | `GD_EVAL_TIMERS` | same as `--timers` |
162
164
 
@@ -168,8 +170,11 @@ A gitignored `.env` at the repo root is the normal place for these; load it with
168
170
  **Adding an evaluator is a registry change, not a naming convention.** Single-shot kinds go
169
171
  into `_EAGER_EVALUATORS`, or `_LAZY_EVALUATOR_MODULES` plus `_LAZY_EVALUATOR_CLASSES`, in
170
172
  `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.
173
+ and a new branch in `_dispatch_agentic`, and must also be classified in `core/interaction.py`:
174
+ add it to `SINGLE_TURN_AGENTIC_KINDS` (one chat message per run) or `MULTI_TURN_KINDS` (a
175
+ simulated user or several turns). That decides which latency section its items land in;
176
+ `test_every_agentic_kind_is_classified_on_purpose` fails until it is done. Test file naming
177
+ follows the capability, but naming a test file correctly registers nothing.
173
178
 
174
179
  **Parallel-safety is a reviewed allowlist, and getting it wrong corrupts results.**
175
180
  `WORKSPACE_MUTATING_TEST_KINDS` is computed as `AGENTIC_TEST_KINDS - PARALLEL_SAFE_TEST_KINDS`,
@@ -178,12 +183,15 @@ That default is correct: agent tool calls create real server-side objects (metri
178
183
  Adding a kind to `PARALLEL_SAFE_TEST_KINDS` is a deliberate assertion that it is read-only,
179
184
  which nothing in the package can prove for you.
180
185
 
181
- **The SSE client's retry predicate is load-bearing.** `core/chat/sse_client.py` retries
182
- 429/502/503/504 and `httpx.RemoteProtocolError` (a mid-stream disconnect) with exponential
183
- backoff, and treats a `METADATA_SYNC_IN_PROGRESS` payload as transient. The
184
- `RemoteProtocolError` case was added after it was confirmed live to contaminate a small
185
- percentage of visualization runs with a hard fail and no retry. Narrowing that predicate
186
- reintroduces the problem.
186
+ **The SSE client's retry predicate is load-bearing.** The backoff loop and the retryable
187
+ status set (429/500/502/503/504) live in `core/_retry.py`, shared by the chat and takeaway
188
+ clients and tuned by the same `GOODDATA_EVAL_CHAT_*` knobs. `core/chat/sse_client.py`
189
+ retries that status set and `httpx.RemoteProtocolError` (a mid-stream disconnect), and
190
+ treats a `METADATA_SYNC_IN_PROGRESS` payload as transient. The `RemoteProtocolError` case
191
+ was added after it was confirmed live to contaminate a small percentage of visualization
192
+ runs with a hard fail and no retry. Narrowing that predicate reintroduces the problem. The
193
+ takeaway client retries the same status set plus connection failures, but not a read
194
+ timeout: a slow page would spend the whole timeout again on every attempt.
187
195
 
188
196
  **Langfuse trace linking is deliberately off the item critical path.** Polling for trace
189
197
  ingestion has no pass/fail signal and inflates or misattributes per-item latency, so
@@ -0,0 +1,6 @@
1
+ # (C) 2026 GoodData Corporation
2
+ include ../../project_common.mk
3
+
4
+ .PHONY: calibrate-takeaway
5
+ calibrate-takeaway:
6
+ uv run --package gooddata-eval --extra llm-judge python scripts/calibrate_takeaway_judge.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gooddata-eval
3
- Version: 1.77.1.dev2
3
+ Version: 1.78.1.dev1
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.dev2
20
+ Requires-Dist: gooddata-sdk~=1.78.1.dev1
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
@@ -161,8 +161,8 @@ gd-eval run \
161
161
  | `--runs K` | `2` | Independent runs per item. |
162
162
  | `--gate` | `any` | Which verdict decides an item: `any` = pass@K (a run passing is enough), `power` = pass^K (every run must pass, so the verdict measures stability). Identical at `--runs 1`. Kinds that repeat K runs only — `power` is refused when the dataset also has non-agentic items or `agentic_conversation`, which are always decided on pass@K. |
163
163
  | `--concurrency K` | `1` | Number of items evaluated concurrently. `1` = sequential (default). Increase to load-test the agent under simultaneous requests — see *Concurrency and workspace safety* below. |
164
- | `--judge-model MODEL` | `gpt-4o` | Model used for LLM-as-judge scoring — `agentic_general_question`, `agentic_guardrail`, `general_question`, `guardrail` and `dashboard_summary`. Also settable via `GD_EVAL_JUDGE_MODEL`. Two things to weigh before changing it: the gpt-5 family rejects `temperature=0`, so verdicts stop being reproducible (the run warns when this happens); and choosing the same model the agent runs means the judge grades its own family's output. |
165
- | `--reasoning-effort LEVEL` | server default | `LOW`, `MEDIUM` or `HIGH`, sent as `options.reasoningEffort` on every chat message. Requires the `enableGenAiReasoningEffort` feature flag on the target organization — without it the server ignores the value. Applies to chat items only; `dashboard_summary` items go through the summary endpoint, which has no such option. |
164
+ | `--judge-model MODEL` | `gpt-4o` | Model used for LLM-as-judge scoring — `agentic_general_question`, `agentic_guardrail`, `general_question`, `guardrail`, `dashboard_summary` and `document_takeaway`. Also settable via `GD_EVAL_JUDGE_MODEL`. Two things to weigh before changing it: the gpt-5 family rejects `temperature=0`, so verdicts stop being reproducible (the run warns when this happens); and choosing the same model the agent runs means the judge grades its own family's output. |
165
+ | `--reasoning-effort LEVEL` | server default | `LOW`, `MEDIUM` or `HIGH`, sent as `options.reasoningEffort` on every chat message. Requires the `enableGenAiReasoningEffort` feature flag on the target organization — without it the server ignores the value. Applies to chat items only; `dashboard_summary` and `document_takeaway` items go through their own endpoints, which have no such option. |
166
166
 
167
167
  **Concurrency and workspace safety.** Agentic kinds that create workspace objects
168
168
  (`agentic_metric_skill`, `agentic_alert_skill`, `agentic_conversation`, `agentic_kda_skill`) always run one at a
@@ -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
@@ -564,7 +736,7 @@ A dataset is a folder of `.json` files, one per question:
564
736
  ```
565
737
 
566
738
  Supported `test_kind` values: `visualization`, `metric_skill`, `alert_skill`,
567
- `search_tool`, `general_question`, `guardrail`, `dashboard_summary`.
739
+ `search_tool`, `general_question`, `guardrail`, `dashboard_summary`, `document_takeaway`.
568
740
 
569
741
  ### `dashboard_summary` items
570
742
 
@@ -603,6 +775,64 @@ The `expected_output` rubric:
603
775
  Each criterion is scored independently by the LLM judge, so `quality_score`
604
776
  is the fraction of satisfied criteria.
605
777
 
778
+ ### `document_takeaway` items
779
+
780
+ Takeaway items call the AI Publisher takeaway endpoint
781
+ (`POST /api/v1/ai/workspaces/{ws}/takeaway`) with a `takeaway_input` block, authored in
782
+ snake_case and sent as the endpoint's camelCase body. In a Langfuse dataset the block goes
783
+ in the item's `metadata` (or its `input` object), and `input` can stay a plain case label;
784
+ an item carrying `takeaway_input` is a `document_takeaway` item unless it declares another
785
+ `test_kind`. The endpoint needs the `enableGenAiReportTakeaways` feature flag.
786
+
787
+ ```json
788
+ {
789
+ "id": "takeaway-001",
790
+ "dataset_name": "agent_document_takeaway",
791
+ "test_kind": "document_takeaway",
792
+ "question": "P01 default format, Sales page, no instructions",
793
+ "takeaway_input": {
794
+ "visualizations": ["<visualization id>", {"visualization_id": "<id>", "filters": []}],
795
+ "filter_context": [],
796
+ "instructions": {"document": null, "slot": null, "format_hint": null},
797
+ "max_characters": 300
798
+ },
799
+ "expected_output": {
800
+ "no_data": false,
801
+ "format": {"no_lists": false},
802
+ "must_include": ["States one finding that connects at least two of the charts"],
803
+ "must_not_include": [],
804
+ "rubric": []
805
+ }
806
+ }
807
+ ```
808
+
809
+ `takeaway_input` requires `visualizations`; unknown keys are rejected. Every
810
+ `expected_output` key is optional:
811
+
812
+ - `no_data` — whether the endpoint must answer `noData`; when either side says no data,
813
+ nothing else is graded.
814
+ - `format` — overrides of the deterministic checks, which gate pass/fail: `bold_headline`,
815
+ `max_headline_words` (10), `max_sentences` (2), `max_words` (50), `single_paragraph`,
816
+ `no_lists`, `no_headings`. `false` or `null` switches a check off; unknown keys are
817
+ rejected. `length` (`max_characters`), `no_ids` and `no_meta` always apply.
818
+ - `default_rules` — `false` drops the default judge criteria (one finding; no executive
819
+ summary, chart walk-through, background opener, figures, dates, causes the data cannot
820
+ show, chart description).
821
+ - `must_include`, `must_not_include`, `rubric` — the item's own judge criteria, graded like
822
+ `dashboard_summary`.
823
+
824
+ `word_count` and `headline_word_count` are reported in the item detail. The default judge
825
+ criteria are calibrated with `make calibrate-takeaway` (needs `OPENAI_API_KEY`); re-run it
826
+ after changing their wording.
827
+
828
+ The client retries 429/5xx answers and connection failures. A request error that survives
829
+ the retries is handled differently by the two entry points: `gd-eval run` aborts the item
830
+ with that error, whatever `--runs` is, while the CI entry point
831
+ (`evaluate_document_takeaway`) records it as an errored run, keeps the item's other runs,
832
+ and raises only when no run completed. A read timeout is not retried, and the CI entry
833
+ point records it as an errored run too. Any other 4xx and an unsupported URL scheme are
834
+ never retried and fail the item in both.
835
+
606
836
  ### Items asked in a dashboard context (`user_context`)
607
837
 
608
838
  An item can be asked the way a user asks from an open dashboard or an attached widget.
@@ -610,7 +840,8 @@ Its `user_context` is sent verbatim as `userContext` on every chat message of th
610
840
  follow-up and clarification messages included — the server treats a message without one
611
841
  as a cleared context. Requires the `enableAiContextSetup` feature flag on the target
612
842
  organization — without it the server ignores the dashboard view. Applies to chat items
613
- only; `dashboard_summary` items carry their scope in `summary_input` instead. No
843
+ only; `dashboard_summary` and `document_takeaway` items carry their scope in
844
+ `summary_input` / `takeaway_input` instead. No
614
845
  `userContext` key is sent at all when neither the item nor, for `agentic_conversation`,
615
846
  the current turn sets one.
616
847
 
@@ -663,6 +894,7 @@ from that turn on until another turn changes it, as the attached context does in
663
894
  | `general_question` | Text answer judged by LLM | `[llm-judge]` |
664
895
  | `guardrail` | Refusal/redirect (visualization response auto-fails) | `[llm-judge]` |
665
896
  | `dashboard_summary` | Dashboard summary (via `/summary` endpoint) scored against a rubric by LLM | `[llm-judge]` |
897
+ | `document_takeaway` | One short takeaway for a page (via `/takeaway` endpoint): deterministic format checks plus a rubric scored by LLM | `[llm-judge]` |
666
898
 
667
899
  ## Optional extras
668
900