gooddata-eval 1.79.2.dev1__tar.gz → 1.80.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 (163) hide show
  1. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/AGENTS.md +1 -1
  2. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/PKG-INFO +4 -3
  3. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/README.md +2 -1
  4. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/pyproject.toml +2 -2
  5. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/cli/agentic_runner.py +54 -0
  6. gooddata_eval-1.80.0/src/gooddata_eval/core/agentic/_editor_takeaways.py +274 -0
  7. gooddata_eval-1.80.0/src/gooddata_eval/core/agentic/_outcome.py +123 -0
  8. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/alert_skill.py +59 -26
  9. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/anomaly_detection.py +56 -31
  10. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/conversation.py +36 -28
  11. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/dashboard_skill.py +386 -28
  12. gooddata_eval-1.80.0/src/gooddata_eval/core/agentic/dashboard_summary.py +581 -0
  13. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/document_skill.py +622 -43
  14. gooddata_eval-1.80.0/src/gooddata_eval/core/agentic/forecasting.py +641 -0
  15. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/general_question.py +57 -18
  16. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/guardrail.py +31 -14
  17. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/kda_skill.py +37 -19
  18. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/metric_skill.py +34 -25
  19. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/search_tool.py +66 -22
  20. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/visualization.py +30 -20
  21. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/what_if.py +57 -32
  22. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/chat/render.py +2 -2
  23. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/dataset/from_insights.py +9 -2
  24. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_llm_judge.py +1 -0
  25. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/interaction.py +2 -0
  26. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/models.py +7 -0
  27. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/takeaway/evaluate.py +2 -9
  28. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/takeaway/http_client.py +22 -3
  29. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/timing.py +12 -1
  30. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_alert_skill.py +61 -6
  31. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_anomaly_detection.py +10 -2
  32. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_dashboard_skill.py +451 -1
  33. gooddata_eval-1.80.0/tests/test_agentic_dashboard_summary.py +368 -0
  34. gooddata_eval-1.80.0/tests/test_agentic_document_editor_takeaways.py +385 -0
  35. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_document_skill.py +987 -3
  36. gooddata_eval-1.80.0/tests/test_agentic_forecasting.py +528 -0
  37. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_general_question.py +72 -0
  38. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_kda_skill.py +2 -0
  39. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_metric_skill.py +37 -0
  40. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_runner.py +112 -7
  41. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_search_tool.py +56 -0
  42. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_what_if.py +12 -2
  43. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_chat_render.py +33 -0
  44. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_from_insights.py +50 -0
  45. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_takeaway_client.py +15 -0
  46. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_timing.py +8 -0
  47. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_trace_linker.py +2 -0
  48. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/.gitignore +0 -0
  49. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/CLAUDE.md +0 -0
  50. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/LICENSE.txt +0 -0
  51. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/Makefile +0 -0
  52. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/scripts/calibrate_takeaway_judge.py +0 -0
  53. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/scripts/verify_guardrail_refusal_criteria.py +0 -0
  54. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/__init__.py +0 -0
  55. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/_version.py +0 -0
  56. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/cli/__init__.py +0 -0
  57. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/cli/main.py +0 -0
  58. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/__init__.py +0 -0
  59. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/_output.py +0 -0
  60. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/_retry.py +0 -0
  61. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/__init__.py +0 -0
  62. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
  63. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_clarification.py +0 -0
  64. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
  65. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
  66. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_gate.py +0 -0
  67. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
  68. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
  69. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
  70. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/chat/__init__.py +0 -0
  71. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/chat/sse_client.py +0 -0
  72. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/config.py +0 -0
  73. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/connection.py +0 -0
  74. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  75. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
  76. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/dataset/local.py +0 -0
  77. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
  78. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  79. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
  80. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
  81. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_rubric.py +0 -0
  82. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  83. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
  84. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/base.py +0 -0
  85. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  86. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  87. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  88. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
  89. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/summary.py +0 -0
  90. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/takeaway.py +0 -0
  91. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
  92. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/granularity.py +0 -0
  93. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  94. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/_env.py +0 -0
  95. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/client.py +0 -0
  96. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
  97. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
  98. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/observations.py +0 -0
  99. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
  100. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  101. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/number_format.py +0 -0
  102. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  103. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/console.py +0 -0
  104. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/html_report.py +0 -0
  105. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/json_report.py +0 -0
  106. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/reporting/report_template.html +0 -0
  107. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/runner.py +0 -0
  108. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/scoring.py +0 -0
  109. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/summary/__init__.py +0 -0
  110. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/summary/http_client.py +0 -0
  111. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/takeaway/__init__.py +0 -0
  112. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/src/gooddata_eval/core/workspace.py +0 -0
  113. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/__init__.py +0 -0
  114. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/_fake_langfuse.py +0 -0
  115. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/conftest.py +0 -0
  116. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  117. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  118. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/fixtures/sse_visualization_stream.txt +0 -0
  119. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/fixtures/takeaway_calibration.json +0 -0
  120. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_conversation.py +0 -0
  121. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_gate.py +0 -0
  122. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_guardrail.py +0 -0
  123. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_join_trace.py +0 -0
  124. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_langfuse_trace.py +0 -0
  125. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_observe_experiment.py +0 -0
  126. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_run_context.py +0 -0
  127. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_agentic_visualization.py +0 -0
  128. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_alert_skill_evaluator.py +0 -0
  129. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_cli.py +0 -0
  130. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_connection.py +0 -0
  131. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_deep_subset.py +0 -0
  132. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_fake_langfuse.py +0 -0
  133. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_guardrail_criteria.py +0 -0
  134. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_html_report.py +0 -0
  135. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_interaction.py +0 -0
  136. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_client.py +0 -0
  137. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_e2e_fake_server.py +0 -0
  138. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_env.py +0 -0
  139. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_experiment.py +0 -0
  140. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_item_scope.py +0 -0
  141. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_observations.py +0 -0
  142. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_otlp.py +0 -0
  143. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_sink.py +0 -0
  144. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_langfuse_source.py +0 -0
  145. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_llm_judge.py +0 -0
  146. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_local_loader.py +0 -0
  147. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_maql_normalize.py +0 -0
  148. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_metric_skill_evaluator.py +0 -0
  149. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_models.py +0 -0
  150. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_number_format.py +0 -0
  151. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_reporting.py +0 -0
  152. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_runner.py +0 -0
  153. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_scoring.py +0 -0
  154. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_search_tool_evaluator.py +0 -0
  155. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_sse_client.py +0 -0
  156. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_summary_client.py +0 -0
  157. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_summary_evaluator.py +0 -0
  158. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_takeaway_evaluate.py +0 -0
  159. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_takeaway_evaluator.py +0 -0
  160. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_text_evaluators.py +0 -0
  161. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_visualization_evaluator.py +0 -0
  162. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tests/test_workspace.py +0 -0
  163. {gooddata_eval-1.79.2.dev1 → gooddata_eval-1.80.0}/tox.ini +0 -0
@@ -27,7 +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
+ | `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. Also used by the agentic document skill to write the takeaways the copilot leaves to the editor, before the narrative is graded |
31
31
  | `core/dataset/` | dataset format, loading, and `from_insights.py` — dataset generation from a workspace's real insights |
32
32
  | `core/evaluators/` | single-shot evaluators and their registry |
33
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) |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gooddata-eval
3
- Version: 1.79.2.dev1
3
+ Version: 1.80.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.79.2.dev1
20
+ Requires-Dist: gooddata-sdk~=1.80.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
@@ -306,7 +306,8 @@ Each item additionally carries a per-phase breakdown:
306
306
  ```
307
307
 
308
308
  Every item reports `runs_ungraded` beside `runs_passed`: runs the agent answered but the LLM judge returned
309
- nothing readable for. Agentic items also list them as `unscored_runs` / `judge_errors` in their `detail`, and a
309
+ nothing readable for. Agentic items also list them as `unscored_runs` / `judge_errors` in their `detail` (a document item whose takeaway-endpoint request failed
310
+ lists that failure under `request_errors` instead), and a
310
311
  `dashboard_summary` item carries `ungraded_criteria`. Such a run — or, for `dashboard_summary`, such a
311
312
  criterion — is excluded from pass@K and from the quality score rather than counted as a failure: scoring it 0
312
313
  would be indistinguishable from the judge genuinely failing the answer, which is the confusion
@@ -277,7 +277,8 @@ Each item additionally carries a per-phase breakdown:
277
277
  ```
278
278
 
279
279
  Every item reports `runs_ungraded` beside `runs_passed`: runs the agent answered but the LLM judge returned
280
- nothing readable for. Agentic items also list them as `unscored_runs` / `judge_errors` in their `detail`, and a
280
+ nothing readable for. Agentic items also list them as `unscored_runs` / `judge_errors` in their `detail` (a document item whose takeaway-endpoint request failed
281
+ lists that failure under `request_errors` instead), and a
281
282
  `dashboard_summary` item carries `ungraded_criteria`. Such a run — or, for `dashboard_summary`, such a
282
283
  criterion — is excluded from pass@K and from the quality score rather than counted as a failure: scoring it 0
283
284
  would be indistinguishable from the judge genuinely failing the answer, which is the confusion
@@ -1,7 +1,7 @@
1
1
  # (C) 2026 GoodData Corporation
2
2
  [project]
3
3
  name = "gooddata-eval"
4
- version = "1.79.2.dev1"
4
+ version = "1.80.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.79.2.dev1",
14
+ "gooddata-sdk~=1.80.0",
15
15
  "httpx>=0.27,<1.0",
16
16
  "jsonpatch>=1.33,<2.0",
17
17
  "orjson>=3.9.15,<4.0.0",
@@ -16,7 +16,9 @@ from gooddata_eval.core.agentic.alert_skill import evaluate_agentic_alert_skill
16
16
  from gooddata_eval.core.agentic.anomaly_detection import evaluate_agentic_anomaly_detection
17
17
  from gooddata_eval.core.agentic.conversation import ConversationFixture, evaluate_agentic_conversation
18
18
  from gooddata_eval.core.agentic.dashboard_skill import evaluate_agentic_dashboard_skill
19
+ from gooddata_eval.core.agentic.dashboard_summary import evaluate_agentic_dashboard_summary
19
20
  from gooddata_eval.core.agentic.document_skill import evaluate_agentic_document_skill
21
+ from gooddata_eval.core.agentic.forecasting import evaluate_agentic_forecasting
20
22
  from gooddata_eval.core.agentic.general_question import evaluate_agentic_general_question
21
23
  from gooddata_eval.core.agentic.guardrail import evaluate_agentic_guardrail
22
24
  from gooddata_eval.core.agentic.kda_skill import evaluate_agentic_kda_skill
@@ -56,6 +58,8 @@ AGENTIC_TEST_KINDS = frozenset(
56
58
  "agentic_guardrail",
57
59
  "agentic_conversation",
58
60
  "agentic_kda_skill",
61
+ "agentic_forecasting",
62
+ "agentic_dashboard_summary",
59
63
  "agentic_anomaly_detection",
60
64
  "agentic_what_if",
61
65
  }
@@ -303,6 +307,41 @@ def _dispatch_agentic(
303
307
  user_context=item.user_context,
304
308
  **lf_kw,
305
309
  )
310
+ elif kind == "agentic_forecasting":
311
+ return evaluate_agentic_forecasting(
312
+ host=host,
313
+ token=token,
314
+ workspace_id=workspace_id,
315
+ question=item.question,
316
+ expected_output=eo if isinstance(eo, dict) else {},
317
+ k=k,
318
+ gate=gate,
319
+ agent_id=agent_id,
320
+ user_context=item.user_context,
321
+ **lf_kw,
322
+ )
323
+ elif kind == "agentic_dashboard_summary":
324
+ summary_input = item.summary_input
325
+ if summary_input is None:
326
+ raise ValueError(f"agentic_dashboard_summary item '{item.id}' is missing required 'summary_input'.")
327
+ return evaluate_agentic_dashboard_summary(
328
+ host=host,
329
+ token=token,
330
+ workspace_id=workspace_id,
331
+ dashboard_id=summary_input.dashboard_id,
332
+ expected_output=eo,
333
+ # The fixture's own wording is the prompt under test -- a localized fixture is
334
+ # only meaningful if its own phrasing is what reaches the agent.
335
+ question=item.question,
336
+ # Each widget costs an execution, so a fixture asserting on a handful of charts
337
+ # can name them instead of paying for the whole dashboard. Same field and meaning
338
+ # the headless /summary endpoint gives it; None summarizes everything.
339
+ only_visualizations=summary_input.visualizations,
340
+ k=k,
341
+ gate=gate,
342
+ agent_id=agent_id,
343
+ **lf_kw,
344
+ )
306
345
  elif kind == "agentic_what_if":
307
346
  return evaluate_agentic_what_if(
308
347
  host=host,
@@ -404,6 +443,18 @@ def _apply_timings(item_report: ItemReport, timings: Any) -> None:
404
443
  item_report.simulated_user_latency_s = timings.simulated_user_s
405
444
 
406
445
 
446
+ def _apply_best_run_latency(item_report: ItemReport, source: Any) -> None:
447
+ """Copy the rank-selected run's own wall time, mirroring the single-shot path's
448
+ ``ItemReport.best_run_latency_s`` (see ``core/runner.py``'s ``_run_one_item``).
449
+
450
+ Kinds not yet wired to measure it pass None and keep the field at its own None
451
+ default rather than reporting an invented number.
452
+ """
453
+ best_run_latency_s = getattr(source, "best_run_latency_s", None)
454
+ if best_run_latency_s is not None:
455
+ item_report.best_run_latency_s = best_run_latency_s
456
+
457
+
407
458
  def run_agentic_items(
408
459
  items: list[DatasetItem],
409
460
  host: str,
@@ -501,6 +552,7 @@ def run_agentic_items(
501
552
  item_report.response_id = response_id
502
553
  item_report.best_detail = detail or {}
503
554
  _apply_timings(item_report, getattr(outcome, "timings", None))
555
+ _apply_best_run_latency(item_report, outcome)
504
556
  _apply_run_counts(item_report, outcome)
505
557
  _apply_failed_runs(item_report, outcome)
506
558
  _apply_run_latencies(item_report, outcome)
@@ -512,6 +564,7 @@ def run_agentic_items(
512
564
  item_report.response_id = getattr(exc, "response_id", None)
513
565
  item_report.best_detail = getattr(exc, "detail", None) or {}
514
566
  _apply_timings(item_report, getattr(exc, "timings", None))
567
+ _apply_best_run_latency(item_report, exc)
515
568
  _apply_run_counts(item_report, exc)
516
569
  _apply_failed_runs(item_report, exc)
517
570
  _apply_run_latencies(item_report, exc)
@@ -555,6 +608,7 @@ def run_agentic_items(
555
608
  # runs and then raised would otherwise show up in the per-response table and not in
556
609
  # the per-conversation one.
557
610
  traces.discard()
611
+ _apply_best_run_latency(item_report, exc)
558
612
  finally:
559
613
  item_report.latency_s = time.perf_counter() - t0
560
614
  item_report.trace_latencies_s = traces.stop()
@@ -0,0 +1,274 @@
1
+ # (C) 2026 GoodData Corporation. All rights reserved.
2
+ """The takeaway request the Publisher editor sends for an unwritten takeaway of a v2 document.
3
+
4
+ Every visualization on the page, in layout order, each under the filters it is drawn with, a
5
+ visualization drawn twice the same way sent once; ``filterContext`` empty; the takeaway's and the
6
+ document's instructions. A visualization is drawn with the period on its own date dataset unless it
7
+ ignores the period, a document date filter on that dataset with both ends set in the period's place
8
+ (keeping its ``emptyValueHandling`` and, for a relative one, its ``boundedFilter``),
9
+ and every document attribute and text filter that narrows the data. An attribute filter excluding
10
+ nothing and a text filter with no value narrow nothing and are left out; measure value filters
11
+ and any other kind are left out too, since the renderer draws no chart differently for them. Widget
12
+ and page filters, and ``ignoreDocumentFilters``, are stored but not applied, so they are not applied
13
+ here either.
14
+
15
+ Two parts of the editor's request are left out: ``maxCharacters``, which the editor derives from the
16
+ layout geometry and the theme, so the endpoint's default length applies; and resolving text variables
17
+ in the instructions, which are sent as written, as the editor does while it has no values for them.
18
+
19
+ Mirrors gdc-ui at commit 7e0f3719ca2 (``documentTakeawayRequest`` and ``copilotTakeaways`` of the
20
+ publisher's v2 editor); check them again when the editor's request changes.
21
+
22
+ Pure: no network.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ from typing import Any
29
+
30
+ from gooddata_eval.core.models import TakeawayInput, TakeawayInstructions, TakeawayVisualization
31
+
32
+ # Publisher periods and dashboard date filters name granularities as GDC.time.*; AFM date filters
33
+ # take the platform enum.
34
+ _AFM_GRANULARITY = {
35
+ "GDC.time.year": "YEAR",
36
+ "GDC.time.quarter": "QUARTER",
37
+ "GDC.time.month": "MONTH",
38
+ "GDC.time.week_us": "WEEK",
39
+ "GDC.time.week": "WEEK",
40
+ "GDC.time.date": "DAY",
41
+ "GDC.time.hour": "HOUR",
42
+ "GDC.time.minute": "MINUTE",
43
+ "GDC.time.second": "SECOND",
44
+ "GDC.time.quarter_in_year": "QUARTER_OF_YEAR",
45
+ "GDC.time.month_in_year": "MONTH_OF_YEAR",
46
+ "GDC.time.week_in_year": "WEEK_OF_YEAR",
47
+ "GDC.time.day_in_year": "DAY_OF_YEAR",
48
+ "GDC.time.day_in_month": "DAY_OF_MONTH",
49
+ "GDC.time.day_in_week": "DAY_OF_WEEK",
50
+ "GDC.time.hour_in_day": "HOUR_OF_DAY",
51
+ "GDC.time.minute_in_hour": "MINUTE_OF_HOUR",
52
+ "GDC.time.minute_in_day": "MINUTE_OF_DAY",
53
+ "GDC.time.second_in_day": "SECOND_OF_DAY",
54
+ "GDC.time.second_in_minute": "SECOND_OF_MINUTE",
55
+ "GDC.time.fiscal_month": "FISCAL_MONTH",
56
+ "GDC.time.fiscal_quarter": "FISCAL_QUARTER",
57
+ "GDC.time.fiscal_year": "FISCAL_YEAR",
58
+ }
59
+
60
+
61
+ class _Unbuildable(Exception):
62
+ """The editor's request has a part this module cannot reproduce."""
63
+
64
+
65
+ def _identifier(reference: Any) -> str | None:
66
+ """The id of a reference written as ``{"identifier": "..."}``, ``{"identifier": {"id": ...}}`` or a bare string."""
67
+ if isinstance(reference, dict):
68
+ reference = reference.get("identifier")
69
+ if isinstance(reference, dict):
70
+ reference = reference.get("id")
71
+ return reference if isinstance(reference, str) else None
72
+
73
+
74
+ def _date_filter(
75
+ dataset: str, kind: Any, granularity: Any, start: Any, end: Any, options: dict[str, Any] | None = None
76
+ ) -> dict:
77
+ """The AFM date filter; ``options`` are the document filter's ``emptyValueHandling`` and ``boundedFilter``."""
78
+ dataset_ref = {"identifier": {"id": dataset, "type": "dataset"}}
79
+ options = options or {}
80
+ if kind == "absolute":
81
+ return {
82
+ "absoluteDateFilter": {
83
+ "dataset": dataset_ref,
84
+ "from": start,
85
+ "to": end,
86
+ **{key: value for key, value in options.items() if key == "emptyValueHandling"},
87
+ }
88
+ }
89
+ if kind == "relative":
90
+ afm_granularity = _AFM_GRANULARITY.get(granularity) if isinstance(granularity, str) else None
91
+ if afm_granularity is None:
92
+ raise _Unbuildable(f"relative date granularity {granularity!r} has no AFM form")
93
+ return {
94
+ "relativeDateFilter": {
95
+ "dataset": dataset_ref,
96
+ "granularity": afm_granularity,
97
+ "from": start,
98
+ "to": end,
99
+ **options,
100
+ }
101
+ }
102
+ raise _Unbuildable(f"date filter type {kind!r} has no AFM form")
103
+
104
+
105
+ def _narrows_the_date(date_filter: dict) -> bool:
106
+ return date_filter.get("from") is not None and date_filter.get("to") is not None
107
+
108
+
109
+ def _date_filters(widget: dict, period: Any, filters: list[Any]) -> list[dict]:
110
+ dataset = _identifier(widget.get("dateDataSet"))
111
+ if dataset is None:
112
+ return []
113
+ for item in filters:
114
+ date_filter = item.get("dateFilter") if isinstance(item, dict) else None
115
+ if (
116
+ isinstance(date_filter, dict)
117
+ and _narrows_the_date(date_filter)
118
+ and _identifier(date_filter.get("dataSet")) == dataset
119
+ ):
120
+ return [
121
+ _date_filter(
122
+ dataset,
123
+ date_filter.get("type"),
124
+ date_filter.get("granularity"),
125
+ date_filter.get("from"),
126
+ date_filter.get("to"),
127
+ {
128
+ key: date_filter[key]
129
+ for key in ("emptyValueHandling", "boundedFilter")
130
+ if date_filter.get(key) is not None
131
+ },
132
+ )
133
+ ]
134
+ if not isinstance(period, dict) or widget.get("ignoreDocumentPeriod") is True:
135
+ return []
136
+ return [_date_filter(dataset, period.get("type"), period.get("granularity"), period.get("from"), period.get("to"))]
137
+
138
+
139
+ _MATCH_TYPES = {"contains": "CONTAINS", "startsWith": "STARTS_WITH", "endsWith": "ENDS_WITH"}
140
+
141
+
142
+ def _label_ref(body: dict) -> dict:
143
+ label = _identifier(body.get("displayForm"))
144
+ if label is None:
145
+ raise _Unbuildable(f"filter {body!r} names no label, so it has no AFM form")
146
+ return {"identifier": {"id": label, "type": "label"}}
147
+
148
+
149
+ def _element_filter(label_ref: dict, values: list[Any], negative: bool) -> dict | None:
150
+ """Selected elements as AFM; None for an exclusion of nothing, which keeps every row."""
151
+ if negative:
152
+ return {"negativeAttributeFilter": {"label": label_ref, "notIn": {"values": values}}} if values else None
153
+ return {"positiveAttributeFilter": {"label": label_ref, "in": {"values": values}}}
154
+
155
+
156
+ def _attribute_filter(body: dict) -> dict | None:
157
+ elements = body.get("attributeElements")
158
+ elements = elements if isinstance(elements, dict) else {}
159
+ values = elements.get("uris") or elements.get("values") or []
160
+ if not isinstance(values, list):
161
+ raise _Unbuildable(f"attribute filter {body!r} has no element list, so it has no AFM form")
162
+ negative = body.get("negativeSelection") is True
163
+ if not values and not negative:
164
+ raise _Unbuildable(f"attribute filter {body!r} selects no element, which has no AFM form")
165
+ return _element_filter(_label_ref(body), values, negative)
166
+
167
+
168
+ def _arbitrary_attribute_filter(body: dict) -> dict | None:
169
+ values = body.get("values")
170
+ if not isinstance(values, list):
171
+ raise _Unbuildable(f"text filter {body!r} has no value list, so it has no AFM form")
172
+ negative = body.get("negativeSelection") is True
173
+ # Tiger drops a text filter without values, so the editor's request carries none.
174
+ return _element_filter(_label_ref(body), values, negative) if values else None
175
+
176
+
177
+ def _match_attribute_filter(body: dict) -> dict:
178
+ operator = body.get("operator")
179
+ match_type = _MATCH_TYPES.get(operator) if isinstance(operator, str) else None
180
+ literal = body.get("literal")
181
+ if match_type is None or not isinstance(literal, str):
182
+ raise _Unbuildable(f"text filter {body!r} has no AFM form")
183
+ return {
184
+ "matchAttributeFilter": {
185
+ "label": _label_ref(body),
186
+ "literal": literal,
187
+ "matchType": match_type,
188
+ "caseSensitive": body.get("caseSensitive") is True,
189
+ "negate": body.get("negativeSelection") is True,
190
+ }
191
+ }
192
+
193
+
194
+ _ATTRIBUTE_KINDS = {
195
+ "attributeFilter": _attribute_filter,
196
+ "arbitraryAttributeFilter": _arbitrary_attribute_filter,
197
+ "matchAttributeFilter": _match_attribute_filter,
198
+ }
199
+
200
+
201
+ def _attribute_filters(filters: list[Any]) -> list[dict]:
202
+ """The AFM form of every document filter other than a date filter that narrows the data."""
203
+ afm: list[dict] = []
204
+ for item in filters:
205
+ if not isinstance(item, dict):
206
+ raise _Unbuildable(f"document filter {item!r} is not an object")
207
+ kind = next((kind for kind in _ATTRIBUTE_KINDS if kind in item), None)
208
+ if kind is None:
209
+ continue
210
+ body = item[kind]
211
+ if not isinstance(body, dict):
212
+ raise _Unbuildable(f"document filter {item!r} has no AFM form")
213
+ converted = _ATTRIBUTE_KINDS[kind](body)
214
+ if converted is not None:
215
+ afm.append(converted)
216
+ return afm
217
+
218
+
219
+ def _visualization_widgets(node: Any) -> list[dict]:
220
+ """Every visualization widget with a visualization under ``node``, in layout order."""
221
+ if not isinstance(node, dict):
222
+ return []
223
+ children = node.get("children")
224
+ if isinstance(children, list):
225
+ return [widget for child in children for widget in _visualization_widgets(child)]
226
+ if node.get("type") == "visualization" and _identifier(node.get("visualization")) is not None:
227
+ return [node]
228
+ return []
229
+
230
+
231
+ def _instruction(text: Any) -> str | None:
232
+ written = text.strip() if isinstance(text, str) else ""
233
+ return written or None
234
+
235
+
236
+ def _generation_instructions(holder: Any) -> Any:
237
+ generation = holder.get("generation") if isinstance(holder, dict) else None
238
+ return generation.get("instructions") if isinstance(generation, dict) else None
239
+
240
+
241
+ def editor_takeaway_input(document: dict, page: dict, takeaway: dict) -> TakeawayInput | str:
242
+ """The request the editor sends to write ``takeaway`` on ``page``, or why it cannot be built.
243
+
244
+ ``document`` is a v2 document in its declarative form, the one the editor reads, with its content
245
+ keys under ``content`` or at the top level.
246
+ """
247
+ content = document.get("content")
248
+ content = content if isinstance(content, dict) else document
249
+ filters = content.get("filters") or []
250
+ if not isinstance(filters, list):
251
+ return f"document filters {filters!r} are not a list"
252
+ try:
253
+ attribute_filters = _attribute_filters(filters)
254
+ placements: dict[str, TakeawayVisualization] = {}
255
+ for widget in _visualization_widgets(page.get("layout")):
256
+ placement = TakeawayVisualization(
257
+ visualization_id=_identifier(widget.get("visualization")) or "",
258
+ filters=[*_date_filters(widget, document.get("period"), filters), *attribute_filters],
259
+ )
260
+ placements.setdefault(json.dumps(placement.model_dump(), sort_keys=True), placement)
261
+ except _Unbuildable as exc:
262
+ return str(exc)
263
+ if not placements:
264
+ return f"page {page.get('id')!r} has no visualization to summarize"
265
+ defaults = content.get("defaults")
266
+ takeaway_defaults = defaults.get("takeaway") if isinstance(defaults, dict) else None
267
+ return TakeawayInput(
268
+ visualizations=list(placements.values()),
269
+ filter_context=[],
270
+ instructions=TakeawayInstructions(
271
+ slot=_instruction(_generation_instructions(takeaway)),
272
+ document=_instruction(_generation_instructions(takeaway_defaults)),
273
+ ),
274
+ )
@@ -0,0 +1,123 @@
1
+ # (C) 2026 GoodData Corporation
2
+ """The common tail of every ``evaluate_agentic_*`` function.
3
+
4
+ Before this module, each of the agentic evaluators hand-wrote the same block: copy
5
+ ``reasoning_steps``/``conversation_id``/``response_id``/``detail``/``runs_passed``/
6
+ ``runs_effective``/``best_run_latency_s`` (and sometimes ``timings``) either onto a
7
+ raised exception or into the returned ``AgenticEvalOutcome``. Eleven independent copies
8
+ of the same ~7 lines meant a new universal field needed eleven edits, not one, and
9
+ nothing failed loudly when a copy was missed -- exactly what happened to ``timings``
10
+ (see ``AgenticAssertionError``'s docstring: "while it lived in eight copies, two
11
+ declared timings and six did not") and again to ``best_run_latency_s`` (every agentic
12
+ kind silently reported it as ``null`` until this was noticed and fixed kind-by-kind).
13
+
14
+ ``agentic_detail`` has the same motivation for the ``detail`` dict's timeline fields:
15
+ ``timeline_detail`` builds both ``latency_breakdown`` and ``tool_calls`` from the same
16
+ events so they stay index-aligned, but roughly half the evaluators called
17
+ ``build_latency_breakdown`` directly and silently never got a ``tool_calls`` key.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any, NoReturn
23
+
24
+ from gooddata_eval.core.models import (
25
+ AgenticAssertionError,
26
+ AgenticEvalOutcome,
27
+ ReasoningStepEvent,
28
+ ToolCallEvent,
29
+ timeline_detail,
30
+ )
31
+ from gooddata_eval.core.timing import PhaseTimings
32
+
33
+ __all__ = ["agentic_detail", "agentic_success", "raise_agentic_failure"]
34
+
35
+
36
+ def agentic_detail(
37
+ tool_call_events: list[ToolCallEvent],
38
+ reasoning_step_events: list[ReasoningStepEvent] | None,
39
+ **kind_specific: Any,
40
+ ) -> dict:
41
+ """A kind's full ``detail`` dict: its own fields plus the universal timeline ones.
42
+
43
+ ``kind_specific`` comes first in the merge so a kind can never accidentally shadow
44
+ ``latency_breakdown``/``tool_calls`` with a same-named field of its own.
45
+ """
46
+ return {**kind_specific, **timeline_detail(tool_call_events, reasoning_step_events)}
47
+
48
+
49
+ def raise_agentic_failure(
50
+ exception_cls: type[AgenticAssertionError],
51
+ message: str,
52
+ *,
53
+ reasoning_steps: list[str],
54
+ conversation_id: str,
55
+ response_id: str | None,
56
+ detail: dict,
57
+ runs_passed: int,
58
+ runs_effective: int,
59
+ best_run_latency_s: float | None,
60
+ timings: PhaseTimings | None = None,
61
+ failed_runs: list[dict] | None = None,
62
+ run_latencies_s: list[float] | None = None,
63
+ ) -> NoReturn:
64
+ """Build ``exception_cls(message)`` with every common field attached, and raise it.
65
+
66
+ ``exception_cls`` must be an ``AgenticAssertionError`` subclass -- that base class is
67
+ what declares these fields as legal targets (see its docstring). A kind whose own
68
+ "no verdict at all" branch raises a bare ``JudgeResponseError`` instead (not a subclass)
69
+ does not go through this helper for that branch.
70
+
71
+ ``timings`` stays optional: only the three kinds that track per-run ``PhaseTimings``
72
+ (general_question, metric_skill, dashboard_skill) pass one -- the rest keep their
73
+ own ``PhaseTimings()`` zero default, same as before this helper existed. ``failed_runs``
74
+ and ``run_latencies_s`` are likewise optional: not every kind has built them by the
75
+ time it raises.
76
+ """
77
+ exc = exception_cls(message)
78
+ exc.reasoning_steps = reasoning_steps
79
+ exc.conversation_id = conversation_id
80
+ exc.response_id = response_id
81
+ exc.detail = detail
82
+ exc.runs_passed = runs_passed
83
+ exc.runs_effective = runs_effective
84
+ exc.best_run_latency_s = best_run_latency_s
85
+ if timings is not None:
86
+ exc.timings = timings
87
+ if failed_runs is not None:
88
+ exc.failed_runs = failed_runs
89
+ if run_latencies_s is not None:
90
+ exc.run_latencies_s = run_latencies_s
91
+ raise exc
92
+
93
+
94
+ def agentic_success(
95
+ *,
96
+ reasoning_steps: list[str],
97
+ conversation_id: str | None,
98
+ response_id: str | None,
99
+ detail: dict,
100
+ runs_passed: int,
101
+ runs_effective: int,
102
+ best_run_latency_s: float | None,
103
+ timings: PhaseTimings | None = None,
104
+ failed_runs: list[dict] | None = None,
105
+ run_latencies_s: list[float] | None = None,
106
+ ) -> AgenticEvalOutcome:
107
+ """The success-path mirror of ``raise_agentic_failure`` -- same fields, same shape."""
108
+ kwargs: dict[str, Any] = {
109
+ "reasoning_steps": reasoning_steps,
110
+ "conversation_id": conversation_id,
111
+ "response_id": response_id,
112
+ "detail": detail,
113
+ "runs_passed": runs_passed,
114
+ "runs_effective": runs_effective,
115
+ "best_run_latency_s": best_run_latency_s,
116
+ }
117
+ if timings is not None:
118
+ kwargs["timings"] = timings
119
+ if failed_runs is not None:
120
+ kwargs["failed_runs"] = failed_runs
121
+ if run_latencies_s is not None:
122
+ kwargs["run_latencies_s"] = run_latencies_s
123
+ return AgenticEvalOutcome(**kwargs)