gooddata-eval 1.76.0__tar.gz → 1.76.1.dev2__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 (135) hide show
  1. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/PKG-INFO +51 -2
  2. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/README.md +49 -0
  3. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/pyproject.toml +2 -2
  4. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/cli/agentic_runner.py +43 -1
  5. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/__init__.py +14 -0
  6. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/alert_skill.py +67 -6
  7. gooddata_eval-1.76.1.dev2/src/gooddata_eval/core/agentic/anomaly_detection.py +650 -0
  8. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/conversation.py +94 -9
  9. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/dashboard_skill.py +9 -1
  10. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/general_question.py +9 -9
  11. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/guardrail.py +9 -1
  12. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/kda_skill.py +39 -3
  13. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/metric_skill.py +98 -3
  14. gooddata_eval-1.76.1.dev2/src/gooddata_eval/core/agentic/report_skill.py +850 -0
  15. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/search_tool.py +9 -1
  16. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/visualization.py +92 -9
  17. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/what_if.py +9 -1
  18. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/chat/sse_client.py +26 -9
  19. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/dataset/langfuse_source.py +12 -3
  20. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/visualization.py +145 -1
  21. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/models.py +41 -0
  22. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/conftest.py +7 -0
  23. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_alert_skill.py +251 -1
  24. gooddata_eval-1.76.1.dev2/tests/test_agentic_anomaly_detection.py +530 -0
  25. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_conversation.py +214 -8
  26. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_general_question.py +0 -18
  27. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_kda_skill.py +12 -1
  28. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_metric_skill.py +184 -1
  29. gooddata_eval-1.76.1.dev2/tests/test_agentic_report_skill.py +843 -0
  30. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_runner.py +92 -9
  31. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_visualization.py +165 -1
  32. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_chat_render.py +42 -1
  33. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_langfuse_source.py +13 -0
  34. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_sse_client.py +32 -9
  35. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_trace_linker.py +2 -0
  36. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_visualization_evaluator.py +160 -2
  37. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/.gitignore +0 -0
  38. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/AGENTS.md +0 -0
  39. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/CLAUDE.md +0 -0
  40. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/LICENSE.txt +0 -0
  41. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/Makefile +0 -0
  42. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/scripts/verify_guardrail_refusal_criteria.py +0 -0
  43. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/__init__.py +0 -0
  44. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/_version.py +0 -0
  45. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/cli/__init__.py +0 -0
  46. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/cli/main.py +0 -0
  47. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/__init__.py +0 -0
  48. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/_output.py +0 -0
  49. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
  50. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
  51. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/_gate.py +0 -0
  52. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
  53. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
  54. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/chat/__init__.py +0 -0
  55. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/chat/render.py +0 -0
  56. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/config.py +0 -0
  57. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/connection.py +0 -0
  58. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  59. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
  60. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/dataset/local.py +0 -0
  61. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
  62. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  63. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
  64. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
  65. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
  66. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  67. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
  68. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/base.py +0 -0
  69. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  70. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  71. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  72. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
  73. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/evaluators/summary.py +0 -0
  74. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/granularity.py +0 -0
  75. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  76. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/langfuse/_env.py +0 -0
  77. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/langfuse/client.py +0 -0
  78. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
  79. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/langfuse/observations.py +0 -0
  80. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
  81. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  82. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  83. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/reporting/console.py +0 -0
  84. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/reporting/html_report.py +0 -0
  85. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/reporting/json_report.py +0 -0
  86. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/reporting/report_template.html +0 -0
  87. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/runner.py +0 -0
  88. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/scoring.py +0 -0
  89. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/summary/__init__.py +0 -0
  90. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/summary/http_client.py +0 -0
  91. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/timing.py +0 -0
  92. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/src/gooddata_eval/core/workspace.py +0 -0
  93. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/__init__.py +0 -0
  94. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/_fake_langfuse.py +0 -0
  95. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  96. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  97. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/fixtures/sse_visualization_stream.txt +0 -0
  98. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_dashboard_skill.py +0 -0
  99. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_gate.py +0 -0
  100. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_guardrail.py +0 -0
  101. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_langfuse_trace.py +0 -0
  102. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_observe_experiment.py +0 -0
  103. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_run_context.py +0 -0
  104. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_search_tool.py +0 -0
  105. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_agentic_what_if.py +0 -0
  106. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_alert_skill_evaluator.py +0 -0
  107. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_cli.py +0 -0
  108. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_connection.py +0 -0
  109. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_deep_subset.py +0 -0
  110. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_fake_langfuse.py +0 -0
  111. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_from_insights.py +0 -0
  112. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_guardrail_criteria.py +0 -0
  113. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_html_report.py +0 -0
  114. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_langfuse_client.py +0 -0
  115. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_langfuse_e2e_fake_server.py +0 -0
  116. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_langfuse_env.py +0 -0
  117. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_langfuse_experiment.py +0 -0
  118. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_langfuse_observations.py +0 -0
  119. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_langfuse_otlp.py +0 -0
  120. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_langfuse_sink.py +0 -0
  121. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_llm_judge.py +0 -0
  122. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_local_loader.py +0 -0
  123. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_maql_normalize.py +0 -0
  124. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_metric_skill_evaluator.py +0 -0
  125. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_models.py +0 -0
  126. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_reporting.py +0 -0
  127. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_runner.py +0 -0
  128. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_scoring.py +0 -0
  129. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_search_tool_evaluator.py +0 -0
  130. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_summary_client.py +0 -0
  131. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_summary_evaluator.py +0 -0
  132. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_text_evaluators.py +0 -0
  133. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_timing.py +0 -0
  134. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tests/test_workspace.py +0 -0
  135. {gooddata_eval-1.76.0 → gooddata_eval-1.76.1.dev2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gooddata-eval
3
- Version: 1.76.0
3
+ Version: 1.76.1.dev2
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.76.0
20
+ Requires-Dist: gooddata-sdk~=1.76.1.dev2
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
@@ -603,6 +603,55 @@ The `expected_output` rubric:
603
603
  Each criterion is scored independently by the LLM judge, so `quality_score`
604
604
  is the fraction of satisfied criteria.
605
605
 
606
+ ### Items asked in a dashboard context (`user_context`)
607
+
608
+ An item can be asked the way a user asks from an open dashboard or an attached widget.
609
+ Its `user_context` is sent verbatim as `userContext` on every chat message of the item,
610
+ follow-up and clarification messages included — the server treats a message without one
611
+ as a cleared context. Requires the `enableAiContextSetup` feature flag on the target
612
+ 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
614
+ `userContext` key is sent at all when neither the item nor, for `agentic_conversation`,
615
+ the current turn sets one.
616
+
617
+ ```json
618
+ {
619
+ "id": "ctx-001",
620
+ "dataset_name": "my_dataset_ctx",
621
+ "test_kind": "general_question",
622
+ "question": "Which dashboard am I looking at, and what does it cover?",
623
+ "user_context": {
624
+ "view": {
625
+ "dashboard": {
626
+ "id": "sales_overview",
627
+ "title": "Sales Overview",
628
+ "widgets": [
629
+ {"widgetType": "insight", "widgetId": "w1", "title": "Revenue by Month", "visualizationId": "revenue_by_month"}
630
+ ]
631
+ }
632
+ }
633
+ },
634
+ "expected_output": "Names the Sales Overview dashboard and summarizes its charts."
635
+ }
636
+ ```
637
+
638
+ The schema belongs to the AI chat API (`UserContext`: `view.dashboard`,
639
+ `referencedObjects`, `activeObject`), so it is not validated here beyond being an object.
640
+ In a Langfuse dataset, put it in the item `metadata` (or in the `input` object) under
641
+ `user_context`. An object in `input` wins and `metadata` is then not read; a missing or
642
+ `null` value in `input` falls back to `metadata`. The value used must be an object or `null`;
643
+ anything else fails the dataset load.
644
+
645
+ `agentic_conversation` items take the item's `user_context` as the context of the first
646
+ turn. A turn in `expected_output.turns` can set its own `user_context`, which applies
647
+ from that turn on until another turn changes it, as the attached context does in the UI:
648
+
649
+ | Turn | Context sent |
650
+ |---|---|
651
+ | no `user_context` key | the current one, unchanged |
652
+ | `"user_context": {...}` | this one, from this turn on |
653
+ | `"user_context": null` | none, from this turn on |
654
+
606
655
  ## Supported test kinds
607
656
 
608
657
  | test_kind | What the agent must produce | Extra required |
@@ -574,6 +574,55 @@ The `expected_output` rubric:
574
574
  Each criterion is scored independently by the LLM judge, so `quality_score`
575
575
  is the fraction of satisfied criteria.
576
576
 
577
+ ### Items asked in a dashboard context (`user_context`)
578
+
579
+ An item can be asked the way a user asks from an open dashboard or an attached widget.
580
+ Its `user_context` is sent verbatim as `userContext` on every chat message of the item,
581
+ follow-up and clarification messages included — the server treats a message without one
582
+ as a cleared context. Requires the `enableAiContextSetup` feature flag on the target
583
+ organization — without it the server ignores the dashboard view. Applies to chat items
584
+ only; `dashboard_summary` items carry their scope in `summary_input` instead. No
585
+ `userContext` key is sent at all when neither the item nor, for `agentic_conversation`,
586
+ the current turn sets one.
587
+
588
+ ```json
589
+ {
590
+ "id": "ctx-001",
591
+ "dataset_name": "my_dataset_ctx",
592
+ "test_kind": "general_question",
593
+ "question": "Which dashboard am I looking at, and what does it cover?",
594
+ "user_context": {
595
+ "view": {
596
+ "dashboard": {
597
+ "id": "sales_overview",
598
+ "title": "Sales Overview",
599
+ "widgets": [
600
+ {"widgetType": "insight", "widgetId": "w1", "title": "Revenue by Month", "visualizationId": "revenue_by_month"}
601
+ ]
602
+ }
603
+ }
604
+ },
605
+ "expected_output": "Names the Sales Overview dashboard and summarizes its charts."
606
+ }
607
+ ```
608
+
609
+ The schema belongs to the AI chat API (`UserContext`: `view.dashboard`,
610
+ `referencedObjects`, `activeObject`), so it is not validated here beyond being an object.
611
+ In a Langfuse dataset, put it in the item `metadata` (or in the `input` object) under
612
+ `user_context`. An object in `input` wins and `metadata` is then not read; a missing or
613
+ `null` value in `input` falls back to `metadata`. The value used must be an object or `null`;
614
+ anything else fails the dataset load.
615
+
616
+ `agentic_conversation` items take the item's `user_context` as the context of the first
617
+ turn. A turn in `expected_output.turns` can set its own `user_context`, which applies
618
+ from that turn on until another turn changes it, as the attached context does in the UI:
619
+
620
+ | Turn | Context sent |
621
+ |---|---|
622
+ | no `user_context` key | the current one, unchanged |
623
+ | `"user_context": {...}` | this one, from this turn on |
624
+ | `"user_context": null` | none, from this turn on |
625
+
577
626
  ## Supported test kinds
578
627
 
579
628
  | test_kind | What the agent must produce | Extra required |
@@ -1,7 +1,7 @@
1
1
  # (C) 2026 GoodData Corporation
2
2
  [project]
3
3
  name = "gooddata-eval"
4
- version = "1.76.0"
4
+ version = "1.76.1.dev2"
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.76.0",
14
+ "gooddata-sdk~=1.76.1.dev2",
15
15
  "httpx>=0.27,<1.0",
16
16
  "jsonpatch>=1.33,<2.0",
17
17
  "orjson>=3.9.15,<4.0.0",
@@ -12,16 +12,19 @@ from gooddata_eval.core.agentic._gate import DEFAULT_GATE, EvalGate, normalize_g
12
12
  from gooddata_eval.core.agentic._langfuse import make_langfuse_client
13
13
  from gooddata_eval.core.agentic._trace_linker import BackgroundTraceLinker, SubmitTraceLink, run_trace_link_inline
14
14
  from gooddata_eval.core.agentic.alert_skill import evaluate_agentic_alert_skill
15
+ from gooddata_eval.core.agentic.anomaly_detection import evaluate_agentic_anomaly_detection
15
16
  from gooddata_eval.core.agentic.conversation import ConversationFixture, evaluate_agentic_conversation
16
17
  from gooddata_eval.core.agentic.dashboard_skill import evaluate_agentic_dashboard_skill
17
18
  from gooddata_eval.core.agentic.general_question import evaluate_agentic_general_question
18
19
  from gooddata_eval.core.agentic.guardrail import evaluate_agentic_guardrail
19
20
  from gooddata_eval.core.agentic.kda_skill import evaluate_agentic_kda_skill
20
21
  from gooddata_eval.core.agentic.metric_skill import evaluate_agentic_metric_skill
22
+ from gooddata_eval.core.agentic.report_skill import evaluate_agentic_report_skill
21
23
  from gooddata_eval.core.agentic.search_tool import evaluate_agentic_search_tool
22
24
  from gooddata_eval.core.agentic.visualization import evaluate_agentic_visualization
23
25
  from gooddata_eval.core.agentic.what_if import evaluate_agentic_what_if
24
26
  from gooddata_eval.core.config import ReasoningEffort
27
+ from gooddata_eval.core.evaluators.visualization import requires_execution_of
25
28
  from gooddata_eval.core.models import AgenticEvalOutcome, CreatedVisualization, DatasetItem
26
29
  from gooddata_eval.core.runner import EvalReport, ItemReport
27
30
 
@@ -43,11 +46,13 @@ AGENTIC_TEST_KINDS = frozenset(
43
46
  "agentic_metric_skill",
44
47
  "agentic_alert_skill",
45
48
  "agentic_dashboard_skill",
49
+ "agentic_report_skill",
46
50
  "agentic_search",
47
51
  "agentic_general_question",
48
52
  "agentic_guardrail",
49
53
  "agentic_conversation",
50
54
  "agentic_kda_skill",
55
+ "agentic_anomaly_detection",
51
56
  "agentic_what_if",
52
57
  }
53
58
  )
@@ -92,7 +97,8 @@ PARALLEL_SAFE_TEST_KINDS = frozenset(
92
97
  #
93
98
  # agentic_dashboard_skill is absent by default rather than by evidence: gen-ai holds the draft and
94
99
  # any chart it authors in conversation state and writes neither until a user saves from the UI, so
95
- # it is a candidate for the allowlist once the dataset has runs behind it.
100
+ # it is a candidate for the allowlist once the dataset has runs behind it. agentic_report_skill is
101
+ # absent for the same reason: the report draft stays in conversation state until a user saves it.
96
102
  WORKSPACE_MUTATING_TEST_KINDS = frozenset(AGENTIC_TEST_KINDS) - PARALLEL_SAFE_TEST_KINDS
97
103
 
98
104
 
@@ -174,9 +180,11 @@ def _dispatch_agentic(
174
180
  workspace_id=workspace_id,
175
181
  question=item.question,
176
182
  expected_outputs=_parse_visualization_expected(eo),
183
+ requires_execution=requires_execution_of(eo),
177
184
  k=k,
178
185
  gate=gate,
179
186
  agent_id=agent_id,
187
+ user_context=item.user_context,
180
188
  **lf_kw,
181
189
  )
182
190
  elif kind == "agentic_metric_skill":
@@ -189,6 +197,7 @@ def _dispatch_agentic(
189
197
  k=k,
190
198
  gate=gate,
191
199
  agent_id=agent_id,
200
+ user_context=item.user_context,
192
201
  **lf_kw,
193
202
  )
194
203
  elif kind == "agentic_dashboard_skill":
@@ -201,6 +210,20 @@ def _dispatch_agentic(
201
210
  k=k,
202
211
  gate=gate,
203
212
  agent_id=agent_id,
213
+ user_context=item.user_context,
214
+ **lf_kw,
215
+ )
216
+ elif kind == "agentic_report_skill":
217
+ return evaluate_agentic_report_skill(
218
+ host=host,
219
+ token=token,
220
+ workspace_id=workspace_id,
221
+ question=item.question,
222
+ expected_output=eo,
223
+ k=k,
224
+ gate=gate,
225
+ agent_id=agent_id,
226
+ user_context=item.user_context,
204
227
  **lf_kw,
205
228
  )
206
229
  elif kind == "agentic_alert_skill":
@@ -213,6 +236,7 @@ def _dispatch_agentic(
213
236
  k=k,
214
237
  gate=gate,
215
238
  agent_id=agent_id,
239
+ user_context=item.user_context,
216
240
  **lf_kw,
217
241
  )
218
242
  elif kind == "agentic_search":
@@ -228,6 +252,7 @@ def _dispatch_agentic(
228
252
  k=k,
229
253
  gate=gate,
230
254
  agent_id=agent_id,
255
+ user_context=item.user_context,
231
256
  **lf_kw,
232
257
  )
233
258
  elif kind == "agentic_general_question":
@@ -253,6 +278,7 @@ def _dispatch_agentic(
253
278
  k=k,
254
279
  gate=gate,
255
280
  agent_id=agent_id,
281
+ user_context=item.user_context,
256
282
  **lf_kw,
257
283
  )
258
284
  elif kind == "agentic_kda_skill":
@@ -265,6 +291,7 @@ def _dispatch_agentic(
265
291
  k=k,
266
292
  gate=gate,
267
293
  agent_id=agent_id,
294
+ user_context=item.user_context,
268
295
  **lf_kw,
269
296
  )
270
297
  elif kind == "agentic_what_if":
@@ -276,6 +303,20 @@ def _dispatch_agentic(
276
303
  expected_output=eo if isinstance(eo, dict) else {},
277
304
  k=k,
278
305
  agent_id=agent_id,
306
+ user_context=item.user_context,
307
+ **lf_kw,
308
+ )
309
+ elif kind == "agentic_anomaly_detection":
310
+ return evaluate_agentic_anomaly_detection(
311
+ host=host,
312
+ token=token,
313
+ workspace_id=workspace_id,
314
+ question=item.question,
315
+ expected_output=eo if isinstance(eo, dict) else {},
316
+ k=k,
317
+ gate=gate,
318
+ agent_id=agent_id,
319
+ user_context=item.user_context,
279
320
  **lf_kw,
280
321
  )
281
322
  elif kind == "agentic_conversation":
@@ -286,6 +327,7 @@ def _dispatch_agentic(
286
327
  workspace_id=workspace_id,
287
328
  fixture=ConversationFixture.model_validate(fixture_data),
288
329
  agent_id=agent_id,
330
+ user_context=item.user_context,
289
331
  **lf_kw,
290
332
  )
291
333
  else:
@@ -53,6 +53,14 @@ from gooddata_eval.core.agentic.metric_skill import (
53
53
  evaluate_agentic_metric_skill,
54
54
  run_agentic_metric_skill,
55
55
  )
56
+ from gooddata_eval.core.agentic.report_skill import (
57
+ AgenticReportSummary,
58
+ ReportEvaluation,
59
+ ReportRunResult,
60
+ ReportSkillAssertionError,
61
+ evaluate_agentic_report_skill,
62
+ run_agentic_report_skill,
63
+ )
56
64
  from gooddata_eval.core.agentic.search_tool import (
57
65
  AgenticSearchSummary,
58
66
  SearchResult,
@@ -75,6 +83,7 @@ __all__ = [
75
83
  "AgenticGuardrailSummary",
76
84
  "AgenticKdaSummary",
77
85
  "AgenticMetricSummary",
86
+ "AgenticReportSummary",
78
87
  "AgenticSearchSummary",
79
88
  "AgenticRunSummary",
80
89
  "AlertEvaluation",
@@ -95,6 +104,9 @@ __all__ = [
95
104
  "KdaSkillAssertionError",
96
105
  "MetricRunResult",
97
106
  "MetricSkillAssertionError",
107
+ "ReportEvaluation",
108
+ "ReportRunResult",
109
+ "ReportSkillAssertionError",
98
110
  "RunResult",
99
111
  "SearchResult",
100
112
  "SearchToolAssertionError",
@@ -108,6 +120,7 @@ __all__ = [
108
120
  "evaluate_agentic_guardrail",
109
121
  "evaluate_agentic_kda_skill",
110
122
  "evaluate_agentic_metric_skill",
123
+ "evaluate_agentic_report_skill",
111
124
  "evaluate_agentic_search_tool",
112
125
  "evaluate_agentic_visualization",
113
126
  "run_agentic_alert_skill",
@@ -117,6 +130,7 @@ __all__ = [
117
130
  "run_agentic_guardrail",
118
131
  "run_agentic_kda_skill",
119
132
  "run_agentic_metric_skill",
133
+ "run_agentic_report_skill",
120
134
  "run_agentic_search_tool",
121
135
  "run_agentic_visualization",
122
136
  ]
@@ -30,11 +30,12 @@ from gooddata_eval.core.agentic._trace_linker import (
30
30
  utc_now,
31
31
  )
32
32
  from gooddata_eval.core.chat.render import render_answer_text
33
- from gooddata_eval.core.chat.sse_client import ChatClient
33
+ from gooddata_eval.core.chat.sse_client import ChatClient, ChatError
34
34
  from gooddata_eval.core.config import ReasoningEffort
35
35
  from gooddata_eval.core.models import (
36
36
  AgenticAssertionError,
37
37
  AgenticEvalOutcome,
38
+ LoopExit,
38
39
  ReasoningStepEvent,
39
40
  ToolCallEvent,
40
41
  shift_and_index_events,
@@ -483,6 +484,12 @@ class AlertRunResult:
483
484
  response_id: str | None = None
484
485
  tool_call_events: list[ToolCallEvent] = field(default_factory=list)
485
486
  reasoning_step_events: list[ReasoningStepEvent] = field(default_factory=list)
487
+ # Why the simulated-user loop stopped, and how many turns it took. Without these a run
488
+ # that ran out of turns is indistinguishable from one that refused: both land on
489
+ # alert_created=False, and every downstream check is `alert_created and ...`, so both
490
+ # also report operator/threshold/metric/recipients as False.
491
+ exit_reason: LoopExit = LoopExit.BUDGET_EXHAUSTED
492
+ turns_used: int = 0
486
493
 
487
494
 
488
495
  @dataclass
@@ -652,12 +659,18 @@ def run_agentic_alert_skill(
652
659
  initial_conversation_id: str | None = None,
653
660
  reasoning_effort: ReasoningEffort | None = None,
654
661
  agent_id: str | None = None,
662
+ user_context: dict | None = None,
655
663
  ) -> AgenticAlertSummary:
656
664
  """Run the alert-skill agentic evaluation K times and return a summary."""
657
665
  expected = _normalize_expected_output(expected_output)
658
666
  run_results: list[AlertRunResult] = []
659
667
  client = ChatClient(
660
- host=host, token=token, workspace_id=workspace_id, reasoning_effort=reasoning_effort, agent_id=agent_id
668
+ host=host,
669
+ token=token,
670
+ workspace_id=workspace_id,
671
+ reasoning_effort=reasoning_effort,
672
+ agent_id=agent_id,
673
+ user_context=user_context,
661
674
  )
662
675
  sdk = GoodDataSdk.create(host, token)
663
676
 
@@ -681,8 +694,35 @@ def run_agentic_alert_skill(
681
694
  turns = 0
682
695
  steps = 0
683
696
 
697
+ # Defaults to BUDGET_EXHAUSTED: every other exit sets it explicitly, so a loop
698
+ # that simply runs out of range() is correctly labelled without a trailing else.
699
+ exit_reason = LoopExit.BUDGET_EXHAUSTED
700
+ turns_used = 0
684
701
  for _iteration in range(max_iterations):
685
- chat_result = client.send_message(conv_id, current_question)
702
+ turns_used = _iteration + 1
703
+ try:
704
+ chat_result = client.send_message(conv_id, current_question)
705
+ except ChatError as exc:
706
+ # Without this the exception escapes run_agentic_alert_skill entirely,
707
+ # discarding every K-run already completed along with any exit_reason.
708
+ print(f"[CHAT] send_message failed for conversation {conv_id}: {exc}")
709
+ # The stream can break AFTER create_metric_alert already succeeded
710
+ # server-side. That id only ever reached alert_id_to_delete from the
711
+ # normal path below, so breaking here left the alert in the workspace for
712
+ # the `finally` cleanup to miss -- a real object leaking out of a failed
713
+ # run, not a reporting gap.
714
+ partial = exc.partial_result
715
+ if partial is not None:
716
+ reasoning_steps.extend(partial.reasoning_steps or [])
717
+ response_id = partial.response_id or response_id
718
+ partial_alert_id, _, partial_tool_called = _extract_alert_call(partial.tool_call_events or [])
719
+ if partial_tool_called:
720
+ alert_id_to_delete = partial_alert_id
721
+ exit_reason = LoopExit.CHAT_ERROR
722
+ break
723
+ # `turns_used` counts attempts (set above, so a failed send still shows the
724
+ # loop reached this iteration); `turns`/`steps` count completed work, so they
725
+ # advance only once send_message has actually returned.
686
726
  turns += 1
687
727
  steps += chat_result.reasoning_step_count
688
728
  reasoning_steps.extend(chat_result.reasoning_steps or [])
@@ -698,6 +738,7 @@ def run_agentic_alert_skill(
698
738
  alert_id, actual_args, tool_called = _extract_alert_call(chat_result.tool_call_events or [])
699
739
  if tool_called:
700
740
  alert_id_to_delete = alert_id
741
+ exit_reason = LoopExit.SUCCESS
701
742
  break
702
743
  response_text = (chat_result.text_response or "").strip()
703
744
  if not response_text and chat_result.alert_proposals:
@@ -706,13 +747,24 @@ def run_agentic_alert_skill(
706
747
  response_text = render_answer_text(chat_result)
707
748
  # Stop if agent gave a completely empty response (stuck)
708
749
  if not response_text and not chat_result.tool_call_events:
750
+ exit_reason = LoopExit.AGENT_SILENT
709
751
  break
710
752
  # Stop before generating a follow-up for the last iteration
711
753
  if _iteration >= max_iterations - 1:
712
754
  break
713
- follow_up = generate_simulated_alert_response(
714
- response_text, expected, conversation_history, question=question
715
- )
755
+ # Recorded rather than raised, matching metric_skill and kda_skill. Letting it
756
+ # propagate did keep a harness fault from being scored as a content failure,
757
+ # but it also discarded the K-runs already completed -- and SIMULATED_USER_FAILED
758
+ # achieves the same separation while keeping them, since reporting reads the
759
+ # exit reason to classify the run as an error rather than an agent failure.
760
+ try:
761
+ follow_up = generate_simulated_alert_response(
762
+ response_text, expected, conversation_history, question=question
763
+ )
764
+ except Exception as exc: # noqa: BLE001 -- harness-side fault; end only this run
765
+ print(f"[SIM-USER] Simulated reply failed for conversation {conv_id}: {exc}")
766
+ exit_reason = LoopExit.SIMULATED_USER_FAILED
767
+ break
716
768
  # Record this exchange so the next call has full history
717
769
  conversation_history.append({"role": "assistant", "content": response_text})
718
770
  conversation_history.append({"role": "user", "content": follow_up})
@@ -740,6 +792,8 @@ def run_agentic_alert_skill(
740
792
  response_id=response_id,
741
793
  tool_call_events=all_tool_call_events,
742
794
  reasoning_step_events=all_reasoning_step_events,
795
+ exit_reason=exit_reason,
796
+ turns_used=turns_used,
743
797
  )
744
798
  finally:
745
799
  if alert_id_to_delete:
@@ -810,6 +864,7 @@ def evaluate_agentic_alert_skill(
810
864
  run_metadata_extra: dict | None = None,
811
865
  reasoning_effort: ReasoningEffort | None = None,
812
866
  submit_trace_link: SubmitTraceLink = run_trace_link_inline,
867
+ user_context: dict | None = None,
813
868
  gate: EvalGate = DEFAULT_GATE,
814
869
  ) -> AgenticEvalOutcome:
815
870
  """Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure.
@@ -833,6 +888,7 @@ def evaluate_agentic_alert_skill(
833
888
  initial_conversation_id=initial_conversation_id,
834
889
  reasoning_effort=reasoning_effort,
835
890
  agent_id=agent_id,
891
+ user_context=user_context,
836
892
  )
837
893
 
838
894
  if langfuse is not None and dataset_item_id:
@@ -908,6 +964,11 @@ def evaluate_agentic_alert_skill(
908
964
  "attributes_correct": ev.attributes_correct,
909
965
  "granularity_correct": ev.granularity_correct,
910
966
  "actual_alert_arguments": best.actual_alert_arguments,
967
+ # Why the loop stopped. alert_created=False alone cannot tell a refusal from a run
968
+ # that hit max_iterations while still on track -- see LoopExit.
969
+ "exit_reason": best.exit_reason.value,
970
+ "turns_used": best.turns_used,
971
+ "max_iterations": max_iterations,
911
972
  **timeline_detail(best.tool_call_events, best.reasoning_step_events),
912
973
  }
913
974