gooddata-eval 1.68.1.dev1__tar.gz → 1.68.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 (84) hide show
  1. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/PKG-INFO +2 -2
  2. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/pyproject.toml +5 -2
  3. gooddata_eval-1.68.1.dev2/src/gooddata_eval/cli/agentic_runner.py +236 -0
  4. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/cli/main.py +49 -3
  5. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/__init__.py +94 -0
  6. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/_catalog.py +49 -0
  7. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/_langfuse.py +396 -0
  8. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/alert_skill.py +498 -0
  9. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/conversation.py +463 -0
  10. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/general_question.py +214 -0
  11. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/guardrail.py +209 -0
  12. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/metric_skill.py +313 -0
  13. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/search_tool.py +207 -0
  14. gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/visualization.py +386 -0
  15. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/chat/sse_client.py +7 -6
  16. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/config.py +1 -0
  17. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/dataset/langfuse_source.py +19 -4
  18. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/visualization.py +27 -5
  19. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/models.py +12 -1
  20. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/scoring.py +4 -3
  21. gooddata_eval-1.68.1.dev2/tests/test_agentic_alert_skill.py +140 -0
  22. gooddata_eval-1.68.1.dev2/tests/test_agentic_conversation.py +172 -0
  23. gooddata_eval-1.68.1.dev2/tests/test_agentic_general_question.py +102 -0
  24. gooddata_eval-1.68.1.dev2/tests/test_agentic_guardrail.py +100 -0
  25. gooddata_eval-1.68.1.dev2/tests/test_agentic_metric_skill.py +146 -0
  26. gooddata_eval-1.68.1.dev2/tests/test_agentic_search_tool.py +111 -0
  27. gooddata_eval-1.68.1.dev2/tests/test_agentic_visualization.py +246 -0
  28. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_sse_client.py +11 -0
  29. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_visualization_evaluator.py +44 -0
  30. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/.gitignore +0 -0
  31. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/LICENSE.txt +0 -0
  32. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/Makefile +0 -0
  33. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/README.md +0 -0
  34. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/__init__.py +0 -0
  35. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/_version.py +0 -0
  36. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/cli/__init__.py +0 -0
  37. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/__init__.py +0 -0
  38. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/chat/__init__.py +0 -0
  39. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/connection.py +0 -0
  40. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  41. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/dataset/local.py +0 -0
  42. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
  43. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  44. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
  45. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  46. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
  47. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/base.py +0 -0
  48. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  49. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  50. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  51. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
  52. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/summary.py +0 -0
  53. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  54. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  55. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  56. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/reporting/console.py +0 -0
  57. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/reporting/json_report.py +0 -0
  58. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/runner.py +0 -0
  59. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/summary/__init__.py +0 -0
  60. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/summary/http_client.py +0 -0
  61. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/workspace.py +0 -0
  62. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/__init__.py +0 -0
  63. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/conftest.py +0 -0
  64. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  65. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  66. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/fixtures/sse_visualization_stream.txt +0 -0
  67. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_alert_skill_evaluator.py +0 -0
  68. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_cli.py +0 -0
  69. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_connection.py +0 -0
  70. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_deep_subset.py +0 -0
  71. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_langfuse_sink.py +0 -0
  72. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_langfuse_source.py +0 -0
  73. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_llm_judge.py +0 -0
  74. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_local_loader.py +0 -0
  75. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_metric_skill_evaluator.py +0 -0
  76. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_models.py +0 -0
  77. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_reporting.py +0 -0
  78. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_runner.py +0 -0
  79. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_scoring.py +0 -0
  80. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_search_tool_evaluator.py +0 -0
  81. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_summary_client.py +0 -0
  82. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_summary_evaluator.py +0 -0
  83. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_text_evaluators.py +0 -0
  84. {gooddata_eval-1.68.1.dev1 → gooddata_eval-1.68.1.dev2}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gooddata-eval
3
- Version: 1.68.1.dev1
3
+ Version: 1.68.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.68.1.dev1
20
+ Requires-Dist: gooddata-sdk~=1.68.1.dev2
21
21
  Requires-Dist: httpx<1.0,>=0.27
22
22
  Requires-Dist: orjson<4.0.0,>=3.9.15
23
23
  Requires-Dist: pydantic<3.0,>=2.6
@@ -1,7 +1,7 @@
1
1
  # (C) 2026 GoodData Corporation
2
2
  [project]
3
3
  name = "gooddata-eval"
4
- version = "1.68.1.dev1"
4
+ version = "1.68.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.68.1.dev1",
14
+ "gooddata-sdk~=1.68.1.dev2",
15
15
  "httpx>=0.27,<1.0",
16
16
  "orjson>=3.9.15,<4.0.0",
17
17
  "pydantic>=2.6,<3.0",
@@ -39,6 +39,9 @@ gd-eval = "gooddata_eval.cli.main:main"
39
39
  Source = "https://github.com/gooddata/gooddata-python-sdk"
40
40
 
41
41
  [dependency-groups]
42
+ dev = [
43
+ "pytest>=8.3.5",
44
+ ]
42
45
  test = [
43
46
  "pytest~=8.3.4",
44
47
  "pytest-cov~=6.0.0",
@@ -0,0 +1,236 @@
1
+ # (C) 2026 GoodData Corporation. All rights reserved.
2
+ """Agentic evaluation runner for gd-eval CLI — handles multi-turn agentic test kinds."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import time
7
+ from typing import Any, TypedDict
8
+
9
+ from gooddata_eval.core.agentic._langfuse import HttpxLangfuseClient, make_langfuse_client
10
+ from gooddata_eval.core.agentic.alert_skill import evaluate_agentic_alert_skill
11
+ from gooddata_eval.core.agentic.conversation import ConversationFixture, evaluate_agentic_conversation
12
+ from gooddata_eval.core.agentic.general_question import evaluate_agentic_general_question
13
+ from gooddata_eval.core.agentic.guardrail import evaluate_agentic_guardrail
14
+ from gooddata_eval.core.agentic.metric_skill import evaluate_agentic_metric_skill
15
+ from gooddata_eval.core.agentic.search_tool import evaluate_agentic_search_tool
16
+ from gooddata_eval.core.agentic.visualization import evaluate_agentic_visualization
17
+ from gooddata_eval.core.models import CreatedVisualization, DatasetItem
18
+ from gooddata_eval.core.runner import EvalReport, ItemReport
19
+
20
+ _LfKw = TypedDict(
21
+ "_LfKw",
22
+ {
23
+ "langfuse": Any,
24
+ "dataset_item_id": str,
25
+ "dataset_name": str,
26
+ "run_timestamp": str,
27
+ "model_version_override": str | None,
28
+ },
29
+ total=False,
30
+ )
31
+
32
+ AGENTIC_TEST_KINDS = frozenset(
33
+ {
34
+ "vis_agentic", # production: expected_output.visualization (single/multi CreatedVisualization)
35
+ "agentic_visualization", # experimental: expected_output.expected_outputs (multi-candidate)
36
+ "agentic_metric_skill",
37
+ "agentic_alert_skill",
38
+ "agentic_search",
39
+ "agentic_general_question",
40
+ "agentic_guardrail",
41
+ "agentic_conversation",
42
+ }
43
+ )
44
+
45
+
46
+ def _parse_visualization_expected(expected_output: Any) -> list[CreatedVisualization]:
47
+ """Parse expected_output into a list of CreatedVisualization candidates.
48
+
49
+ Accepts:
50
+ {"expected_outputs": [{"visualization": {...}}, ...]} <- agentic fixture format
51
+ {"visualization": {...}} or {"visualization": [{...}]} <- single/multi candidate
52
+ [{"visualization": {...}}, ...] <- bare list
53
+ """
54
+ if isinstance(expected_output, dict):
55
+ raw_list = expected_output.get("expected_outputs")
56
+ if raw_list is not None:
57
+ return [
58
+ CreatedVisualization.model_validate(v.get("visualization", v) if isinstance(v, dict) else v)
59
+ for v in raw_list
60
+ ]
61
+ raw_viz = expected_output.get("visualization")
62
+ if raw_viz is not None:
63
+ if isinstance(raw_viz, list):
64
+ return [CreatedVisualization.model_validate(v) for v in raw_viz]
65
+ return [CreatedVisualization.model_validate(raw_viz)]
66
+ if isinstance(expected_output, list):
67
+ return [
68
+ CreatedVisualization.model_validate(v.get("visualization", v) if isinstance(v, dict) else v)
69
+ for v in expected_output
70
+ ]
71
+ raise ValueError(
72
+ f"Cannot parse agentic_visualization expected_output: {type(expected_output).__name__}. "
73
+ 'Expected {"expected_outputs": [...]} or {"visualization": {...}}.'
74
+ )
75
+
76
+
77
+ def _dispatch_agentic(
78
+ item: DatasetItem,
79
+ host: str,
80
+ token: str,
81
+ workspace_id: str,
82
+ k: int,
83
+ langfuse: Any,
84
+ run_ts: str,
85
+ model_version_override: str | None,
86
+ ) -> None:
87
+ """Call the appropriate evaluate_agentic_* function for the item's test_kind."""
88
+ kind = item.test_kind
89
+ eo = item.expected_output
90
+ lf_kw: _LfKw = {
91
+ "langfuse": langfuse,
92
+ "dataset_item_id": item.id,
93
+ "dataset_name": item.dataset_name,
94
+ "run_timestamp": run_ts,
95
+ "model_version_override": model_version_override,
96
+ }
97
+
98
+ if kind in ("vis_agentic", "agentic_visualization"):
99
+ evaluate_agentic_visualization(
100
+ host=host,
101
+ token=token,
102
+ workspace_id=workspace_id,
103
+ question=item.question,
104
+ expected_outputs=_parse_visualization_expected(eo),
105
+ k=k,
106
+ **lf_kw,
107
+ )
108
+ elif kind == "agentic_metric_skill":
109
+ evaluate_agentic_metric_skill(
110
+ host=host,
111
+ token=token,
112
+ workspace_id=workspace_id,
113
+ question=item.question,
114
+ expected_output=eo if isinstance(eo, (dict, list)) else {},
115
+ k=k,
116
+ **lf_kw,
117
+ )
118
+ elif kind == "agentic_alert_skill":
119
+ evaluate_agentic_alert_skill(
120
+ host=host,
121
+ token=token,
122
+ workspace_id=workspace_id,
123
+ question=item.question,
124
+ expected_output=eo if isinstance(eo, dict) else {},
125
+ k=k,
126
+ **lf_kw,
127
+ )
128
+ elif kind == "agentic_search":
129
+ eo_dict = eo if isinstance(eo, dict) else {}
130
+ tool_call = eo_dict.get("tool_call", {})
131
+ expected_args = tool_call.get("function_arguments", eo_dict)
132
+ evaluate_agentic_search_tool(
133
+ host=host,
134
+ token=token,
135
+ workspace_id=workspace_id,
136
+ question=item.question,
137
+ expected_tool_call=expected_args,
138
+ k=k,
139
+ **lf_kw,
140
+ )
141
+ elif kind == "agentic_general_question":
142
+ evaluate_agentic_general_question(
143
+ host=host,
144
+ token=token,
145
+ workspace_id=workspace_id,
146
+ question=item.question,
147
+ expected_output=eo if isinstance(eo, str) else str(eo),
148
+ k=k,
149
+ **lf_kw,
150
+ )
151
+ elif kind == "agentic_guardrail":
152
+ evaluate_agentic_guardrail(
153
+ host=host,
154
+ token=token,
155
+ workspace_id=workspace_id,
156
+ question=item.question,
157
+ expected_output=eo if isinstance(eo, str) else str(eo),
158
+ k=k,
159
+ **lf_kw,
160
+ )
161
+ elif kind == "agentic_conversation":
162
+ fixture_data = eo.get("fixture") or eo if isinstance(eo, dict) else {}
163
+ evaluate_agentic_conversation(
164
+ host=host,
165
+ token=token,
166
+ workspace_id=workspace_id,
167
+ fixture=ConversationFixture.model_validate(fixture_data),
168
+ **lf_kw,
169
+ )
170
+ else:
171
+ raise ValueError(f"Unknown agentic test kind: {kind!r}")
172
+
173
+
174
+ def run_agentic_items(
175
+ items: list[DatasetItem],
176
+ host: str,
177
+ token: str,
178
+ workspace_id: str,
179
+ *,
180
+ k: int = 2,
181
+ model_version: str | None = None,
182
+ use_langfuse: bool = False,
183
+ run_ts: str,
184
+ on_item_start: Any = None,
185
+ on_item_done: Any = None,
186
+ ) -> EvalReport:
187
+ """Run agentic items through evaluate_agentic_* and return an EvalReport."""
188
+ langfuse = make_langfuse_client() if use_langfuse else None
189
+
190
+ report = EvalReport(model=model_version)
191
+ total = len(items)
192
+
193
+ for index, item in enumerate(items, start=1):
194
+ if on_item_start is not None:
195
+ try:
196
+ on_item_start(index, total, item)
197
+ except Exception:
198
+ pass
199
+
200
+ item_report = ItemReport(
201
+ id=item.id,
202
+ dataset_name=item.dataset_name,
203
+ test_kind=item.test_kind,
204
+ question=item.question,
205
+ )
206
+ t0 = time.perf_counter()
207
+ try:
208
+ _dispatch_agentic(item, host, token, workspace_id, k, langfuse, run_ts, model_version)
209
+ item_report.pass_at_k = True
210
+ item_report.runs = k
211
+ except AssertionError as exc:
212
+ item_report.pass_at_k = False
213
+ item_report.runs = k
214
+ print(f"[agentic] {item.id} FAIL: {exc}", flush=True)
215
+ except Exception as exc:
216
+ item_report.error = f"{type(exc).__name__}: {exc}"
217
+ item_report.runs = 0
218
+ finally:
219
+ item_report.latency_s = time.perf_counter() - t0
220
+
221
+ if on_item_done is not None:
222
+ try:
223
+ on_item_done(index, total, item_report)
224
+ except Exception:
225
+ pass
226
+
227
+ report.items.append(item_report)
228
+
229
+ if langfuse is not None:
230
+ try:
231
+ langfuse.flush()
232
+ langfuse.close()
233
+ except Exception:
234
+ pass
235
+
236
+ return report
@@ -19,6 +19,7 @@ from gooddata_eval.core.langfuse.sink import LangfuseSink
19
19
  from gooddata_eval.core.models import ChatResult, DatasetItem
20
20
  from gooddata_eval.core.reporting.console import render_comparison, render_console
21
21
  from gooddata_eval.core.reporting.json_report import write_multi_model_report
22
+ from gooddata_eval.cli.agentic_runner import AGENTIC_TEST_KINDS, run_agentic_items
22
23
  from gooddata_eval.core.runner import ItemReport, run_items
23
24
  from gooddata_eval.core.summary.http_client import SummaryClient
24
25
  from gooddata_eval.core.workspace import ModelResolutionError, WorkspaceModelController
@@ -62,6 +63,17 @@ def _build_parser() -> argparse.ArgumentParser:
62
63
  source = run.add_mutually_exclusive_group(required=True)
63
64
  source.add_argument("--dataset", help="Path to a folder of dataset JSON files.")
64
65
  source.add_argument("--langfuse-dataset", dest="langfuse_dataset", help="Langfuse dataset name.")
66
+ run.add_argument(
67
+ "--kind",
68
+ dest="kind",
69
+ default="visualization",
70
+ metavar="TEST_KIND",
71
+ help=(
72
+ "Default test kind for dataset items that don't embed one. "
73
+ "Use 'vis_agentic', 'agentic_visualization', 'agentic_metric_skill', etc. for multi-turn agentic eval. "
74
+ "(default: visualization)"
75
+ ),
76
+ )
65
77
  run.add_argument(
66
78
  "--model",
67
79
  action="append",
@@ -165,7 +177,7 @@ def _load_dataset(config: RunConfig):
165
177
 
166
178
  if config.langfuse_dataset is None: # pragma: no cover - argparse mutually-exclusive group guarantees one is set
167
179
  raise ValueError("Either --dataset or --langfuse-dataset is required.")
168
- return load_langfuse_dataset(config.langfuse_dataset)
180
+ return load_langfuse_dataset(config.langfuse_dataset, default_test_kind=config.kind)
169
181
 
170
182
 
171
183
  def _list_models(host: str, token: str, workspace_id: str | None) -> int:
@@ -228,6 +240,8 @@ def _run(config: RunConfig) -> int:
228
240
  return _EXIT_OPERATIONAL_ERROR
229
241
 
230
242
  items = _load_dataset(config)
243
+ agentic_items = [i for i in items if i.test_kind in AGENTIC_TEST_KINDS]
244
+ non_agentic_items = [i for i in items if i.test_kind not in AGENTIC_TEST_KINDS]
231
245
  models = config.models or []
232
246
  run_ts = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H-%M")
233
247
  n_models = len(models) if models else 1
@@ -287,13 +301,30 @@ def _run(config: RunConfig) -> int:
287
301
  ) -> None:
288
302
  _sink.log_item(report, dataset_item_id=report.id)
289
303
 
304
+ # --- agentic items (multi-turn, use evaluate_agentic_*) ---
305
+ agentic_report = None
306
+ if agentic_items:
307
+ agentic_report = run_agentic_items(
308
+ agentic_items,
309
+ host=config.host,
310
+ token=config.token,
311
+ workspace_id=config.workspace_id,
312
+ k=config.runs,
313
+ model_version=resolved.model_id,
314
+ use_langfuse=config.log_to_langfuse,
315
+ run_ts=run_ts,
316
+ on_item_start=on_item_start,
317
+ on_item_done=on_item_done,
318
+ )
319
+
320
+ # --- non-agentic items (single-turn, use Evaluator) ---
290
321
  backend = _RoutingBackend(
291
322
  ChatClient(host=config.host, token=config.token, workspace_id=config.workspace_id),
292
323
  SummaryClient(host=config.host, token=config.token, workspace_id=config.workspace_id),
293
324
  )
294
325
  try:
295
- report = run_items(
296
- items,
326
+ single_report = run_items(
327
+ non_agentic_items,
297
328
  backend,
298
329
  runs=config.runs,
299
330
  model=resolved.model_id,
@@ -310,6 +341,20 @@ def _run(config: RunConfig) -> int:
310
341
  if hasattr(backend, "close"):
311
342
  backend.close()
312
343
 
344
+ # merge into a single report for display/export
345
+ from gooddata_eval.core.runner import EvalReport # noqa: PLC0415
346
+
347
+ report = EvalReport(
348
+ model=resolved.model_id,
349
+ provider_name=resolved.provider_name or resolved.provider_id,
350
+ provider_type=resolved.provider_type,
351
+ workspace_id=config.workspace_id,
352
+ )
353
+ if agentic_report is not None:
354
+ report.items.extend(agentic_report.items)
355
+ report.items.extend(single_report.items)
356
+ report.wall_clock_s = (agentic_report.wall_clock_s if agentic_report else 0.0) + single_report.wall_clock_s
357
+
313
358
  skipped_kinds = sorted({i.test_kind for i in report.items if i.skipped})
314
359
  if skipped_kinds:
315
360
  print(
@@ -363,6 +408,7 @@ def main(argv: list[str] | None = None) -> int:
363
408
  json_path=Path(args.json_path) if args.json_path else None,
364
409
  log_to_langfuse=args.langfuse,
365
410
  quiet=args.quiet,
411
+ kind=args.kind,
366
412
  )
367
413
  return _run(config)
368
414
  except (
@@ -0,0 +1,94 @@
1
+ # (C) 2026 GoodData Corporation. All rights reserved.
2
+ from gooddata_eval.core.agentic.alert_skill import (
3
+ AgenticAlertSummary,
4
+ AlertEvaluation,
5
+ AlertRunResult,
6
+ AlertSkillAssertionError,
7
+ evaluate_agentic_alert_skill,
8
+ run_agentic_alert_skill,
9
+ )
10
+ from gooddata_eval.core.agentic.conversation import (
11
+ ConversationAssertionError,
12
+ ConversationFixture,
13
+ ConversationResult,
14
+ TurnDefinition,
15
+ TurnResult,
16
+ evaluate_agentic_conversation,
17
+ run_agentic_conversation,
18
+ )
19
+ from gooddata_eval.core.agentic.general_question import (
20
+ AgenticGeneralQuestionSummary,
21
+ GeneralQuestionAssertionError,
22
+ GeneralQuestionResult,
23
+ evaluate_agentic_general_question,
24
+ run_agentic_general_question,
25
+ )
26
+ from gooddata_eval.core.agentic.guardrail import (
27
+ AgenticGuardrailSummary,
28
+ GuardrailAssertionError,
29
+ GuardrailResult,
30
+ evaluate_agentic_guardrail,
31
+ run_agentic_guardrail,
32
+ )
33
+ from gooddata_eval.core.agentic.metric_skill import (
34
+ AgenticMetricSummary,
35
+ MetricRunResult,
36
+ MetricSkillAssertionError,
37
+ evaluate_agentic_metric_skill,
38
+ run_agentic_metric_skill,
39
+ )
40
+ from gooddata_eval.core.agentic.search_tool import (
41
+ AgenticSearchSummary,
42
+ SearchResult,
43
+ SearchToolAssertionError,
44
+ evaluate_agentic_search_tool,
45
+ run_agentic_search_tool,
46
+ )
47
+ from gooddata_eval.core.agentic.visualization import (
48
+ AgenticRunSummary,
49
+ RunResult,
50
+ VisualizationAssertionError,
51
+ evaluate_agentic_visualization,
52
+ run_agentic_visualization,
53
+ )
54
+
55
+ __all__ = [
56
+ "AgenticAlertSummary",
57
+ "AgenticGeneralQuestionSummary",
58
+ "AgenticGuardrailSummary",
59
+ "AgenticMetricSummary",
60
+ "AgenticSearchSummary",
61
+ "AgenticRunSummary",
62
+ "AlertEvaluation",
63
+ "AlertRunResult",
64
+ "AlertSkillAssertionError",
65
+ "ConversationAssertionError",
66
+ "ConversationFixture",
67
+ "ConversationResult",
68
+ "GeneralQuestionAssertionError",
69
+ "GeneralQuestionResult",
70
+ "GuardrailAssertionError",
71
+ "GuardrailResult",
72
+ "MetricRunResult",
73
+ "MetricSkillAssertionError",
74
+ "RunResult",
75
+ "SearchResult",
76
+ "SearchToolAssertionError",
77
+ "TurnDefinition",
78
+ "TurnResult",
79
+ "VisualizationAssertionError",
80
+ "evaluate_agentic_alert_skill",
81
+ "evaluate_agentic_conversation",
82
+ "evaluate_agentic_general_question",
83
+ "evaluate_agentic_guardrail",
84
+ "evaluate_agentic_metric_skill",
85
+ "evaluate_agentic_search_tool",
86
+ "evaluate_agentic_visualization",
87
+ "run_agentic_alert_skill",
88
+ "run_agentic_conversation",
89
+ "run_agentic_general_question",
90
+ "run_agentic_guardrail",
91
+ "run_agentic_metric_skill",
92
+ "run_agentic_search_tool",
93
+ "run_agentic_visualization",
94
+ ]
@@ -0,0 +1,49 @@
1
+ # (C) 2026 GoodData Corporation. All rights reserved.
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ @dataclass
8
+ class CatalogMetricAlert:
9
+ """Eval fixture schema for create_metric_alert tool arguments.
10
+
11
+ This is an eval-specific type, not a gooddata-sdk API entity. It holds the
12
+ flat expected output from a YAML fixture and is never serialised to the API.
13
+ """
14
+
15
+ operator: str = "GREATER_THAN"
16
+ """Comparison operator: GREATER_THAN, LESS_THAN, EQUAL_TO, BETWEEN, NOT_BETWEEN."""
17
+ threshold: float | int | None = None
18
+ """Threshold value for single-sided operators (GREATER_THAN, LESS_THAN, EQUAL_TO)."""
19
+ threshold_from: float | int | None = None
20
+ """Lower bound for BETWEEN / NOT_BETWEEN operators."""
21
+ threshold_to: float | int | None = None
22
+ """Upper bound for BETWEEN / NOT_BETWEEN operators."""
23
+ trigger: str = "not specified"
24
+ """Alert trigger mode: ALWAYS, ONCE, or 'not specified' (defaults to ALWAYS)."""
25
+ metric_id: str | None = None
26
+ """Identifier of the metric the alert monitors."""
27
+ recipients: list[str] = field(default_factory=list)
28
+ """List of recipient email addresses."""
29
+ filters: list | str | None = None
30
+ """Attribute filters applied to the alert condition."""
31
+
32
+ @classmethod
33
+ def from_dict(cls, d: dict) -> CatalogMetricAlert:
34
+ """Build a CatalogMetricAlert from a canonical (lowercase-key) dict."""
35
+ recipients_raw = d.get("recipients") or []
36
+ if isinstance(recipients_raw, str):
37
+ recipients = [r.strip() for r in recipients_raw.replace(";", ",").split(",") if r.strip()]
38
+ else:
39
+ recipients = list(recipients_raw)
40
+ return cls(
41
+ operator=d.get("operator", "GREATER_THAN"),
42
+ threshold=d.get("threshold"),
43
+ threshold_from=d.get("threshold_from"),
44
+ threshold_to=d.get("threshold_to"),
45
+ trigger=d.get("trigger", "not specified"),
46
+ metric_id=d.get("metric_id"),
47
+ recipients=recipients,
48
+ filters=d.get("filters"),
49
+ )