gooddata-eval 1.68.0__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.
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/PKG-INFO +2 -2
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/pyproject.toml +5 -2
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/cli/agentic_runner.py +236 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/cli/main.py +49 -3
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/__init__.py +94 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/_catalog.py +49 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/_langfuse.py +396 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/alert_skill.py +498 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/conversation.py +463 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/general_question.py +214 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/guardrail.py +209 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/metric_skill.py +313 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/search_tool.py +207 -0
- gooddata_eval-1.68.1.dev2/src/gooddata_eval/core/agentic/visualization.py +386 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/chat/sse_client.py +7 -6
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/config.py +1 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/dataset/langfuse_source.py +19 -4
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/visualization.py +27 -5
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/models.py +12 -1
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/scoring.py +4 -3
- gooddata_eval-1.68.1.dev2/tests/test_agentic_alert_skill.py +140 -0
- gooddata_eval-1.68.1.dev2/tests/test_agentic_conversation.py +172 -0
- gooddata_eval-1.68.1.dev2/tests/test_agentic_general_question.py +102 -0
- gooddata_eval-1.68.1.dev2/tests/test_agentic_guardrail.py +100 -0
- gooddata_eval-1.68.1.dev2/tests/test_agentic_metric_skill.py +146 -0
- gooddata_eval-1.68.1.dev2/tests/test_agentic_search_tool.py +111 -0
- gooddata_eval-1.68.1.dev2/tests/test_agentic_visualization.py +246 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_sse_client.py +11 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_visualization_evaluator.py +44 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/.gitignore +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/LICENSE.txt +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/Makefile +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/README.md +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/evaluators/summary.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/reporting/console.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/reporting/json_report.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/runner.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/summary/http_client.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/__init__.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/conftest.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_alert_skill_evaluator.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_cli.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_connection.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_langfuse_source.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_models.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_reporting.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_runner.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_summary_client.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.68.0 → gooddata_eval-1.68.1.dev2}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.68.0 → 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
296
|
-
|
|
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
|
+
)
|