gooddata-eval 1.78.1.dev4__tar.gz → 1.79.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 (155) hide show
  1. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/PKG-INFO +2 -2
  2. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/pyproject.toml +2 -2
  3. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/dashboard_skill.py +300 -41
  4. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_dashboard_skill.py +366 -1
  5. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/.gitignore +0 -0
  6. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/AGENTS.md +0 -0
  7. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/CLAUDE.md +0 -0
  8. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/LICENSE.txt +0 -0
  9. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/Makefile +0 -0
  10. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/README.md +0 -0
  11. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/scripts/calibrate_takeaway_judge.py +0 -0
  12. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/scripts/verify_guardrail_refusal_criteria.py +0 -0
  13. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/__init__.py +0 -0
  14. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/_version.py +0 -0
  15. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/cli/__init__.py +0 -0
  16. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/cli/agentic_runner.py +0 -0
  17. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/cli/main.py +0 -0
  18. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/__init__.py +0 -0
  19. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/_output.py +0 -0
  20. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/_retry.py +0 -0
  21. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/__init__.py +0 -0
  22. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
  23. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
  24. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
  25. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/_gate.py +0 -0
  26. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
  27. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
  28. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/alert_skill.py +0 -0
  29. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/anomaly_detection.py +0 -0
  30. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/conversation.py +0 -0
  31. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/document_skill.py +0 -0
  32. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/general_question.py +0 -0
  33. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/guardrail.py +0 -0
  34. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/kda_skill.py +0 -0
  35. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/metric_skill.py +0 -0
  36. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
  37. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/search_tool.py +0 -0
  38. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/visualization.py +0 -0
  39. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/agentic/what_if.py +0 -0
  40. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/chat/__init__.py +0 -0
  41. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/chat/render.py +0 -0
  42. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/chat/sse_client.py +0 -0
  43. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/config.py +0 -0
  44. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/connection.py +0 -0
  45. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  46. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
  47. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
  48. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/dataset/local.py +0 -0
  49. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
  50. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  51. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
  52. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
  53. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
  54. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/_rubric.py +0 -0
  55. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  56. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
  57. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/base.py +0 -0
  58. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  59. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  60. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  61. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
  62. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/summary.py +0 -0
  63. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/takeaway.py +0 -0
  64. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
  65. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/granularity.py +0 -0
  66. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/interaction.py +0 -0
  67. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  68. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/langfuse/_env.py +0 -0
  69. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/langfuse/client.py +0 -0
  70. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
  71. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
  72. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/langfuse/observations.py +0 -0
  73. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
  74. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  75. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/models.py +0 -0
  76. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/number_format.py +0 -0
  77. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  78. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/reporting/console.py +0 -0
  79. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/reporting/html_report.py +0 -0
  80. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/reporting/json_report.py +0 -0
  81. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/reporting/report_template.html +0 -0
  82. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/runner.py +0 -0
  83. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/scoring.py +0 -0
  84. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/summary/__init__.py +0 -0
  85. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/summary/http_client.py +0 -0
  86. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/takeaway/__init__.py +0 -0
  87. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/takeaway/evaluate.py +0 -0
  88. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/takeaway/http_client.py +0 -0
  89. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/timing.py +0 -0
  90. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/src/gooddata_eval/core/workspace.py +0 -0
  91. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/__init__.py +0 -0
  92. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/_fake_langfuse.py +0 -0
  93. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/conftest.py +0 -0
  94. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  95. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  96. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/fixtures/sse_visualization_stream.txt +0 -0
  97. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/fixtures/takeaway_calibration.json +0 -0
  98. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_alert_skill.py +0 -0
  99. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_anomaly_detection.py +0 -0
  100. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_conversation.py +0 -0
  101. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_document_skill.py +0 -0
  102. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_gate.py +0 -0
  103. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_general_question.py +0 -0
  104. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_guardrail.py +0 -0
  105. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_join_trace.py +0 -0
  106. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_kda_skill.py +0 -0
  107. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_langfuse_trace.py +0 -0
  108. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_metric_skill.py +0 -0
  109. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_observe_experiment.py +0 -0
  110. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_run_context.py +0 -0
  111. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_runner.py +0 -0
  112. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_search_tool.py +0 -0
  113. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_visualization.py +0 -0
  114. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_agentic_what_if.py +0 -0
  115. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_alert_skill_evaluator.py +0 -0
  116. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_chat_render.py +0 -0
  117. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_cli.py +0 -0
  118. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_connection.py +0 -0
  119. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_deep_subset.py +0 -0
  120. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_fake_langfuse.py +0 -0
  121. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_from_insights.py +0 -0
  122. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_guardrail_criteria.py +0 -0
  123. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_html_report.py +0 -0
  124. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_interaction.py +0 -0
  125. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_client.py +0 -0
  126. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_e2e_fake_server.py +0 -0
  127. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_env.py +0 -0
  128. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_experiment.py +0 -0
  129. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_item_scope.py +0 -0
  130. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_observations.py +0 -0
  131. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_otlp.py +0 -0
  132. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_sink.py +0 -0
  133. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_langfuse_source.py +0 -0
  134. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_llm_judge.py +0 -0
  135. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_local_loader.py +0 -0
  136. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_maql_normalize.py +0 -0
  137. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_metric_skill_evaluator.py +0 -0
  138. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_models.py +0 -0
  139. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_number_format.py +0 -0
  140. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_reporting.py +0 -0
  141. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_runner.py +0 -0
  142. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_scoring.py +0 -0
  143. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_search_tool_evaluator.py +0 -0
  144. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_sse_client.py +0 -0
  145. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_summary_client.py +0 -0
  146. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_summary_evaluator.py +0 -0
  147. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_takeaway_client.py +0 -0
  148. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_takeaway_evaluate.py +0 -0
  149. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_takeaway_evaluator.py +0 -0
  150. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_text_evaluators.py +0 -0
  151. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_timing.py +0 -0
  152. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_trace_linker.py +0 -0
  153. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_visualization_evaluator.py +0 -0
  154. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tests/test_workspace.py +0 -0
  155. {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1.dev1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gooddata-eval
3
- Version: 1.78.1.dev4
3
+ Version: 1.79.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.78.1.dev4
20
+ Requires-Dist: gooddata-sdk~=1.79.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
@@ -1,7 +1,7 @@
1
1
  # (C) 2026 GoodData Corporation
2
2
  [project]
3
3
  name = "gooddata-eval"
4
- version = "1.78.1.dev4"
4
+ version = "1.79.1.dev1"
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.78.1.dev4",
14
+ "gooddata-sdk~=1.79.1.dev1",
15
15
  "httpx>=0.27,<1.0",
16
16
  "jsonpatch>=1.33,<2.0",
17
17
  "orjson>=3.9.15,<4.0.0",
@@ -313,20 +313,33 @@ def _grouped_filter_entries(dashboard: dict) -> list[tuple[str, dict, str | None
313
313
  if tabs
314
314
  else [("", dashboard.get("filters") or {})]
315
315
  )
316
- entries: list[tuple[str, dict, str | None]] = []
317
- for where, filters in maps:
318
- for key, value in filters.items():
319
- if not isinstance(value, dict):
320
- continue
321
- label = where or f"filter {key!r}"
322
- if value.get("type") != "filter_group":
323
- entries.append((label, value, None))
324
- continue
325
- group = str(value.get("title") or "")
326
- entries.extend(
327
- (label, member, group) for member in (value.get("filters") or {}).values() if isinstance(member, dict)
328
- )
329
- return entries
316
+ return [
317
+ (where or f"filter {entry_key!r}", value, group)
318
+ for where, filters in maps
319
+ for entry_key, _key, value, group in _members_of(filters)
320
+ ]
321
+
322
+
323
+ def _members_of(filters: dict) -> list[tuple[str, str, dict, str | None]]:
324
+ """``(entry key, filter key, filter, group title)`` for every filter of one filter map.
325
+
326
+ A group's members are read in its place, each under its own key and with the group's key as
327
+ the entry key; a filter outside a group is its own entry and has no group title.
328
+ """
329
+ members: list[tuple[str, str, dict, str | None]] = []
330
+ for entry_key, value in filters.items():
331
+ if not isinstance(value, dict):
332
+ continue
333
+ if value.get("type") != "filter_group":
334
+ members.append((entry_key, entry_key, value, None))
335
+ continue
336
+ group = str(value.get("title") or "")
337
+ members.extend(
338
+ (entry_key, key, member, group)
339
+ for key, member in (value.get("filters") or {}).items()
340
+ if isinstance(member, dict)
341
+ )
342
+ return members
330
343
 
331
344
 
332
345
  def _filters_of_type(dashboard: dict, filter_type: str) -> list[tuple[str, dict]]:
@@ -549,6 +562,11 @@ def _matching_widgets(widgets: list[dict], new_ids: set[str], expected: dict) ->
549
562
  return [w for w in widgets if w.get("visualization") in new_ids and _norm(str(w.get("title") or "")) == title]
550
563
 
551
564
 
565
+ def _chart_entry(chart: str | dict) -> dict:
566
+ """A chart as ``tabs`` and ``layout`` name it -- an id, or ``{"title": ...}`` -- as a ``_matching_widgets`` entry."""
567
+ return {"id": chart} if isinstance(chart, str) else {"id": None, **chart}
568
+
569
+
552
570
  def _has_date_bindings(expected_output: dict) -> bool:
553
571
  """Whether any expected chart states the date dataset its widget must follow.
554
572
 
@@ -665,10 +683,11 @@ def _check_tabs(dashboard: dict, new_ids: set[str], expected: list[dict]) -> lis
665
683
  if "title" in exp and _norm(str(tab.get("title") or "")) != _norm(str(exp["title"] or "")):
666
684
  failures.append(f"tab {index} is titled {tab.get('title')!r}, expected {exp['title']!r}")
667
685
  tab_widgets = [w for section in tab.get("sections") or [] for w in section.get("widgets") or []]
668
- for chart in exp.get("visualizations") or []:
669
- entry = {"id": chart} if isinstance(chart, str) else {"id": None, **chart}
670
- if not _matching_widgets(tab_widgets, new_ids, entry):
671
- failures.append(f"tab {index} ({tab.get('title')!r}) does not hold chart {chart!r}")
686
+ failures.extend(
687
+ f"tab {index} ({tab.get('title')!r}) does not hold chart {chart!r}"
688
+ for chart in exp.get("visualizations") or []
689
+ if not _matching_widgets(tab_widgets, new_ids, _chart_entry(chart))
690
+ )
672
691
  return failures
673
692
 
674
693
 
@@ -732,27 +751,10 @@ def _check_tab_filters(dashboard: dict, expected: dict[str, dict]) -> list[str]:
732
751
  Exact on purpose: GDAI-2540 is a filter that turns up on a tab it never belonged to, which a
733
752
  check of only the expected entries would pass. Keyed by the saved dashboard's local filter
734
753
  id rather than by label, so two filters on the same label stay distinguishable.
754
+
755
+ It is ``filter_bar`` with every filter known by key, so the two read filter groups the same way.
735
756
  """
736
- tabs = {tab.get("id"): tab for tab in dashboard.get("tabs") or []}
737
- failures: list[str] = []
738
- for tab_id, expected_filters in expected.items():
739
- tab = tabs.get(tab_id)
740
- if tab is None:
741
- failures.append(f"the dashboard has no tab {tab_id!r}")
742
- continue
743
- actual = {k: v for k, v in (tab.get("filters") or {}).items() if isinstance(v, dict)}
744
- failures.extend(
745
- f"tab {tab_id!r} carries filter {filter_id!r}, which it should not"
746
- for filter_id in sorted(actual.keys() - expected_filters.keys())
747
- )
748
- failures.extend(
749
- f"tab {tab_id!r} lost filter {filter_id!r}" for filter_id in sorted(expected_filters.keys() - actual.keys())
750
- )
751
- for filter_id in sorted(expected_filters.keys() & actual.keys()):
752
- failures.extend(
753
- _filter_mismatch(f"tab {tab_id!r} filter {filter_id!r}", actual[filter_id], expected_filters[filter_id])
754
- )
755
- return failures
757
+ return _check_filter_bar(dashboard, [{"tab": tab_id, "filters": filters} for tab_id, filters in expected.items()])
756
758
 
757
759
 
758
760
  def _filters_by_id(dashboard: dict) -> dict[str, list[tuple[str, dict]]]:
@@ -819,6 +821,167 @@ def _check_preserved_widgets(widgets: list[dict], expected: list[dict]) -> list[
819
821
  return failures
820
822
 
821
823
 
824
+ def _named_tab(dashboard: dict, tab_id: str | None) -> tuple[dict | None, str]:
825
+ """The tab an entry of ``layout`` or ``filter_bar`` names, and how to refer to it in a failure.
826
+
827
+ An entry without ``tab`` is for a dashboard with one tab only, root sections included: on a
828
+ dashboard with several it would be a guess which one the case meant, so it is a failure.
829
+ """
830
+ tabs = _tabs_of(dashboard)
831
+ if tab_id is None:
832
+ if len(tabs) != 1:
833
+ return None, f"the dashboard has {len(tabs)} tabs, so the expectation must name one"
834
+ return tabs[0], "the dashboard"
835
+ tab = next((t for t in tabs if t.get("id") == tab_id), None)
836
+ return tab, f"tab {tab_id!r}" if tab is not None else f"the dashboard has no tab {tab_id!r}"
837
+
838
+
839
+ def _check_layout(dashboard: dict, new_ids: set[str], expected: list[dict]) -> list[str]:
840
+ """Each named tab must hold exactly the expected sections, in order, each with exactly its charts in order.
841
+
842
+ Exact on purpose. A move that leaves the widget where it was, places a second copy, or puts
843
+ it in the wrong section keeps every chart "on the dashboard", and so does a removal that
844
+ never happened; only the whole arrangement tells them apart. A section's ``title`` is
845
+ compared when the entry states it, ``null`` meaning untitled. Its ``visualizations`` are
846
+ always stated, ``[]`` for a section without charts. A chart is an id, or
847
+ ``{"title": ...}`` for one the agent authored. Widgets that show no visualization are not
848
+ charts and are left out on both sides.
849
+ """
850
+ failures: list[str] = []
851
+ for entry in expected:
852
+ tab, where = _named_tab(dashboard, entry.get("tab"))
853
+ if tab is None:
854
+ failures.append(where)
855
+ continue
856
+ sections = tab.get("sections") or []
857
+ wanted = entry["sections"]
858
+ if len(sections) != len(wanted):
859
+ titles = ", ".join(repr(s.get("title")) for s in sections)
860
+ failures.append(f"{where} has {len(sections)} section(s) ({titles}), expected {len(wanted)}")
861
+ continue
862
+ for index, (section, want) in enumerate(zip(sections, wanted, strict=True)):
863
+ if "title" in want and _norm(str(section.get("title") or "")) != _norm(str(want["title"] or "")):
864
+ failures.append(
865
+ f"{where} section {index} is titled {section.get('title')!r}, expected {want['title']!r}"
866
+ )
867
+ charts = [w for w in section.get("widgets") or [] if w.get("visualization")]
868
+ charts_wanted = want["visualizations"]
869
+ if len(charts) != len(charts_wanted) or not all(
870
+ _matching_widgets([w], new_ids, _chart_entry(c)) for w, c in zip(charts, charts_wanted, strict=True)
871
+ ):
872
+ shown = [w.get("visualization") for w in charts]
873
+ failures.append(f"{where} section {index} holds {shown}, expected {charts_wanted}")
874
+ return failures
875
+
876
+
877
+ def _check_filter_bar(dashboard: dict, expected: list[dict]) -> list[str]:
878
+ """Each named tab must carry exactly the expected filters: the kept ones by key, the new ones by content.
879
+
880
+ ``filters`` names a filter the case knows the key of, the saved one or one the change keeps.
881
+ ``added`` holds a filter whose key the agent mints at run time -- a new one, or one it
882
+ replaced by removing and re-adding it -- each matched to a different filter that ``filters``
883
+ did not claim. Anything left over fails: a filter that should have been removed, a duplicate,
884
+ or one copied in from elsewhere. Unlike ``tab_filters``, it reads a dashboard without tabs too.
885
+
886
+ A grouped filter is read by its own key. Its entry names the group's title as ``group``, and
887
+ an entry without one must sit outside every group, so a filter moved into or out of a group
888
+ fails. A group none of the expected filters sits in fails like a leftover filter -- an empty
889
+ one, or one whose members should have been ungrouped -- and so does a key the tab carries twice.
890
+ """
891
+ failures: list[str] = []
892
+ for entry in expected:
893
+ tab, where = _named_tab(dashboard, entry.get("tab"))
894
+ if tab is None:
895
+ failures.append(where)
896
+ continue
897
+ filters = tab.get("filters") or {}
898
+ groups = {
899
+ key: str(value.get("title") or "")
900
+ for key, value in filters.items()
901
+ if isinstance(value, dict) and value.get("type") == "filter_group"
902
+ }
903
+ # Each filter by its own key, with the key of the group it sits in.
904
+ actual: dict[str, tuple[dict, str | None]] = {}
905
+ for entry_key, key, value, group in _members_of(filters):
906
+ if key in actual:
907
+ failures.append(f"{where} carries filter {key!r} more than once")
908
+ continue
909
+ actual[key] = (value, None if group is None else entry_key)
910
+ claimed: set[str | None] = set()
911
+ for filter_id, want in (entry.get("filters") or {}).items():
912
+ got = actual.pop(filter_id, None)
913
+ if got is None:
914
+ failures.append(f"{where} lost filter {filter_id!r}")
915
+ continue
916
+ value, group_key = got
917
+ failures.extend(_filter_mismatch(f"{where} filter {filter_id!r}", value, _without_group(want)))
918
+ if _in_wanted_group(groups, group_key, want):
919
+ claimed.add(group_key)
920
+ else:
921
+ sits = "no group" if group_key is None else f"group {groups[group_key]!r}"
922
+ wanted = f"group {want['group']!r}" if "group" in want else "no group"
923
+ failures.append(f"{where} filter {filter_id!r} sits in {sits}, expected {wanted}")
924
+ added = entry.get("added") or []
925
+ keys = list(actual)
926
+ fits = [
927
+ [
928
+ not _filter_mismatch("", actual[key][0], _without_group(want))
929
+ and _in_wanted_group(groups, actual[key][1], want)
930
+ for key in keys
931
+ ]
932
+ for want in added
933
+ ]
934
+ for want, index in zip(added, _one_to_one(fits), strict=True):
935
+ if index is None:
936
+ failures.append(f"{where} carries no new filter matching {want!r}")
937
+ continue
938
+ claimed.add(actual.pop(keys[index])[1])
939
+ failures.extend(f"{where} carries filter {key!r}, which it should not" for key in sorted(actual))
940
+ failures.extend(
941
+ f"{where} carries filter group {key!r} ({title!r}), which holds none of the expected filters"
942
+ for key, title in sorted(groups.items())
943
+ if key not in claimed
944
+ )
945
+ return failures
946
+
947
+
948
+ def _without_group(expected: dict) -> dict:
949
+ return {k: v for k, v in expected.items() if k != "group"}
950
+
951
+
952
+ def _in_wanted_group(groups: dict[str, str], group_key: str | None, expected: dict) -> bool:
953
+ """Whether a filter in the group keyed ``group_key`` (``None`` for none) sits where ``expected`` puts it."""
954
+ if group_key is None or "group" not in expected:
955
+ return group_key is None and "group" not in expected
956
+ return _norm(groups[group_key]) == _norm(expected["group"])
957
+
958
+
959
+ def _one_to_one(fits: list[list[bool]]) -> list[int | None]:
960
+ """For each row, a column it fits, no column taken twice, with as many rows matched as there can be.
961
+
962
+ First fit is not enough: an expectation that names fewer keys can take the filter a stricter
963
+ one needed and fail a correct dashboard on the order the entries are listed in. Each row
964
+ that finds every fitting column taken tries to move that column's owner to another one.
965
+ """
966
+ owner: dict[int, int] = {}
967
+
968
+ def claim(row: int, seen: set[int]) -> bool:
969
+ for column, fit in enumerate(fits[row]):
970
+ if fit and column not in seen:
971
+ seen.add(column)
972
+ if column not in owner or claim(owner[column], seen):
973
+ owner[column] = row
974
+ return True
975
+ return False
976
+
977
+ for row in range(len(fits)):
978
+ claim(row, set())
979
+ matched: list[int | None] = [None] * len(fits)
980
+ for column, row in owner.items():
981
+ matched[row] = column
982
+ return matched
983
+
984
+
822
985
  def _date_range_text(date_range: dict | None) -> str:
823
986
  """Render a date range the way a user would say it, for the simulated reply.
824
987
 
@@ -986,7 +1149,7 @@ def _validate_expectation(expected_output: dict) -> None:
986
1149
 
987
1150
 
988
1151
  def _validate_layout_expectations(expected_output: dict) -> None:
989
- """Shape checks for the opt-in keys on charts, tabs, per-tab filters and preserved widgets.
1152
+ """Shape checks for the opt-in keys on charts, tabs, per-tab filters, preserved widgets, layout and filter bar.
990
1153
 
991
1154
  Raises:
992
1155
  ValueError: one of them is unusable. Loud for the same reason as the rest of the
@@ -1023,8 +1186,7 @@ def _validate_layout_expectations(expected_output: dict) -> None:
1023
1186
  ):
1024
1187
  raise ValueError(f"tab_filters[{tab_id!r}] must map filter ids to typed filters, got {filters!r}")
1025
1188
  for filter_id, entry in filters.items():
1026
- if entry["type"] == "attribute_filter":
1027
- _validate_tab_attribute_filter(f"tab_filters[{tab_id!r}][{filter_id!r}]", entry)
1189
+ _validate_bar_filter(f"tab_filters[{tab_id!r}][{filter_id!r}]", entry)
1028
1190
 
1029
1191
  if "date_filters" in expected_output:
1030
1192
  date_filters = expected_output["date_filters"]
@@ -1057,6 +1219,87 @@ def _validate_layout_expectations(expected_output: dict) -> None:
1057
1219
  if not isinstance(widget, dict) or not widget.get("visualization") or len(widget) < 2:
1058
1220
  raise ValueError(f"a preserved widget needs its visualization and a key to guard, got {widget!r}")
1059
1221
 
1222
+ if "layout" in expected_output:
1223
+ for entry in _tab_entries(expected_output, "layout", {"tab", "sections"}):
1224
+ sections = entry.get("sections")
1225
+ if not isinstance(sections, list):
1226
+ raise ValueError(f"a layout entry needs its sections as a list, got {entry!r}")
1227
+ for section in sections:
1228
+ if not isinstance(section, dict) or section.keys() - {"title", "visualizations"}:
1229
+ raise ValueError(f"a layout section takes only title and visualizations, got {section!r}")
1230
+ if section.get("title") is not None and not isinstance(section["title"], str):
1231
+ raise ValueError(f"a layout section's title must be a string or null, got {section!r}")
1232
+ if "visualizations" not in section:
1233
+ # Missing would read as "no charts" here but as "any charts" in `tabs`.
1234
+ raise ValueError(f"a layout section must list its visualizations, [] for none, got {section!r}")
1235
+ charts = section["visualizations"]
1236
+ if not isinstance(charts, list) or not all(
1237
+ (isinstance(c, str) and c)
1238
+ or (isinstance(c, dict) and c.keys() == {"title"} and isinstance(c["title"], str) and c["title"])
1239
+ for c in charts
1240
+ ):
1241
+ raise ValueError(
1242
+ f"a layout section's visualizations must be chart ids or {{'title': ...}}, got {section!r}"
1243
+ )
1244
+
1245
+ if "filter_bar" in expected_output:
1246
+ for entry in _tab_entries(expected_output, "filter_bar", {"tab", "filters", "added"}):
1247
+ where = f"filter_bar[{entry.get('tab')!r}]"
1248
+ kept = entry.get("filters", {})
1249
+ added = entry.get("added", [])
1250
+ if not isinstance(kept, dict) or not isinstance(added, list):
1251
+ raise ValueError(f"{where} needs filters as a map of keys and added as a list, got {entry!r}")
1252
+ named = [(f"{where}[{key!r}]", f) for key, f in kept.items()]
1253
+ named += [(f"{where} added[{index}]", f) for index, f in enumerate(added)]
1254
+ for label, body in named:
1255
+ _validate_bar_filter(label, body)
1256
+
1257
+
1258
+ def _validate_bar_filter(label: str, body: object) -> None:
1259
+ """A filter ``tab_filters`` or ``filter_bar`` expects: typed, in the AAC shape, and never a group.
1260
+
1261
+ A group is not a filter there: both checks read its members by their own keys, so an
1262
+ expected ``filter_group`` could never match. Its members say which group they sit in.
1263
+
1264
+ Raises:
1265
+ ValueError: the filter is unusable.
1266
+ """
1267
+ if not isinstance(body, dict) or not body.get("type"):
1268
+ raise ValueError(f"{label} must be a typed filter, got {body!r}")
1269
+ if body["type"] == "filter_group":
1270
+ raise ValueError(f"{label} is a filter_group; list its members instead, each with group set to its title")
1271
+ if "group" in body and not (isinstance(body["group"], str) and body["group"]):
1272
+ raise ValueError(f"{label} group must be the title of the filter group, got {body['group']!r}")
1273
+ if body["type"] == "attribute_filter":
1274
+ _validate_tab_attribute_filter(label, body)
1275
+
1276
+
1277
+ def _tab_entries(expected_output: dict, key: str, allowed: set[str]) -> list[dict]:
1278
+ """The entries of a per-tab expectation, checked for the shape ``layout`` and ``filter_bar`` share.
1279
+
1280
+ Raises:
1281
+ ValueError: not a non-empty list of objects, a key outside ``allowed``, or a tab named
1282
+ twice. An entry without ``tab`` is for a dashboard with one tab, so it must be the
1283
+ only entry: beside another, both could name that one tab.
1284
+ """
1285
+ entries = expected_output[key]
1286
+ if not isinstance(entries, list) or not entries or not all(isinstance(e, dict) for e in entries):
1287
+ raise ValueError(f"{key} must be a non-empty list of per-tab objects, got {entries!r}")
1288
+ seen: set[str | None] = set()
1289
+ for entry in entries:
1290
+ unknown = sorted(entry.keys() - allowed)
1291
+ if unknown:
1292
+ raise ValueError(f"a {key} entry cannot state {unknown}, got {entry!r}")
1293
+ tab = entry.get("tab")
1294
+ if tab is not None and not (isinstance(tab, str) and tab):
1295
+ raise ValueError(f"a {key} entry's tab must be a tab id, got {entry!r}")
1296
+ if tab in seen:
1297
+ raise ValueError(f"{key} names tab {tab!r} more than once")
1298
+ seen.add(tab)
1299
+ if None in seen and len(entries) > 1:
1300
+ raise ValueError(f"a {key} entry without tab must be the only entry, got {entries!r}")
1301
+ return entries
1302
+
1060
1303
 
1061
1304
  def _is_dataset_or_null(value: object) -> bool:
1062
1305
  return value is None or (isinstance(value, str) and bool(value))
@@ -1103,6 +1346,8 @@ class _Applies:
1103
1346
  date_filters: bool = False
1104
1347
  preserved_widgets: bool = False
1105
1348
  chart_types: bool = False
1349
+ layout: bool = False
1350
+ filter_bar: bool = False
1106
1351
 
1107
1352
  @classmethod
1108
1353
  def of(cls, expected_output: dict) -> _Applies:
@@ -1116,6 +1361,8 @@ class _Applies:
1116
1361
  date_filters="date_filters" in expected_output,
1117
1362
  preserved_widgets="preserved_widgets" in expected_output,
1118
1363
  chart_types=_has_chart_types(expected_output),
1364
+ layout="layout" in expected_output,
1365
+ filter_bar="filter_bar" in expected_output,
1119
1366
  )
1120
1367
 
1121
1368
 
@@ -1157,6 +1404,8 @@ class DashboardEvaluation:
1157
1404
  date_filters_correct: bool = False
1158
1405
  widgets_preserved: bool = False
1159
1406
  chart_types_correct: bool = False
1407
+ layout_correct: bool = False
1408
+ filter_bar_correct: bool = False
1160
1409
  failures: list[str] = field(default_factory=list)
1161
1410
  notes: list[str] = field(default_factory=list)
1162
1411
 
@@ -1199,6 +1448,10 @@ class DashboardEvaluation:
1199
1448
  checks["dashboard_widgets_preserved"] = self.widgets_preserved
1200
1449
  if self.applies.chart_types:
1201
1450
  checks["dashboard_chart_types_correct"] = self.chart_types_correct
1451
+ if self.applies.layout:
1452
+ checks["dashboard_layout_correct"] = self.layout_correct
1453
+ if self.applies.filter_bar:
1454
+ checks["dashboard_filter_bar_correct"] = self.filter_bar_correct
1202
1455
  return checks
1203
1456
 
1204
1457
  @property
@@ -1414,6 +1667,8 @@ def evaluate_dashboard_response(
1414
1667
  if applies.chart_types
1415
1668
  else ([], [])
1416
1669
  )
1670
+ layout_failures = _check_layout(document, new_ids, expected_output["layout"]) if applies.layout else []
1671
+ filter_bar_failures = _check_filter_bar(document, expected_output["filter_bar"]) if applies.filter_bar else []
1417
1672
 
1418
1673
  return DashboardEvaluation(
1419
1674
  drafted=True,
@@ -1434,6 +1689,8 @@ def evaluate_dashboard_response(
1434
1689
  date_filters_correct=not date_filter_failures,
1435
1690
  widgets_preserved=not preserved_failures,
1436
1691
  chart_types_correct=not chart_type_failures,
1692
+ layout_correct=not layout_failures,
1693
+ filter_bar_correct=not filter_bar_failures,
1437
1694
  failures=[
1438
1695
  *chart_failures,
1439
1696
  *saved_failures,
@@ -1445,6 +1702,8 @@ def evaluate_dashboard_response(
1445
1702
  *date_filter_failures,
1446
1703
  *preserved_failures,
1447
1704
  *chart_type_failures,
1705
+ *layout_failures,
1706
+ *filter_bar_failures,
1448
1707
  *new_failures,
1449
1708
  ],
1450
1709
  notes=[*title_notes, *reference_notes, *chart_type_notes],