gooddata-eval 1.69.0__tar.gz → 1.69.1.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/PKG-INFO +2 -2
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/pyproject.toml +3 -2
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/cli/agentic_runner.py +9 -12
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/cli/main.py +14 -2
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/alert_skill.py +9 -5
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/conversation.py +3 -1
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/general_question.py +3 -1
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/guardrail.py +3 -1
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/metric_skill.py +4 -3
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/search_tool.py +3 -1
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/visualization.py +3 -1
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/chat/sse_client.py +32 -5
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/config.py +1 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/alert_skill.py +3 -1
- gooddata_eval-1.69.1.dev1/src/gooddata_eval/core/evaluators/search_tool.py +59 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/models.py +2 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/json_report.py +2 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/runner.py +7 -1
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_alert_skill.py +20 -2
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_conversation.py +1 -2
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_general_question.py +0 -2
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_guardrail.py +0 -2
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_search_tool.py +0 -2
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_visualization.py +5 -8
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_alert_skill_evaluator.py +17 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_cli.py +67 -8
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_langfuse_source.py +2 -1
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_runner.py +25 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_sse_client.py +118 -0
- gooddata_eval-1.69.1.dev1/tox.ini +16 -0
- gooddata_eval-1.69.0/src/gooddata_eval/core/evaluators/search_tool.py +0 -40
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/.gitignore +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/LICENSE.txt +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/Makefile +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/README.md +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/summary.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/console.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/scoring.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/summary/http_client.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/__init__.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/conftest.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_metric_skill.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_connection.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_models.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_reporting.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_summary_client.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_visualization_evaluator.py +0 -0
- {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.69.
|
|
3
|
+
Version: 1.69.1.dev1
|
|
4
4
|
Summary: Evaluate the GoodData AI agent against your own questions and models.
|
|
5
5
|
Project-URL: Source, https://github.com/gooddata/gooddata-python-sdk
|
|
6
6
|
Author-email: GoodData <support@gooddata.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
17
17
|
Classifier: Topic :: Software Development
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist: gooddata-sdk~=1.69.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.69.1.dev1
|
|
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.69.
|
|
4
|
+
version = "1.69.1.dev1"
|
|
5
5
|
description = "Evaluate the GoodData AI agent against your own questions and models."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
keywords = ["gooddata", "ai", "evaluation", "llm", "analytics", "cli"]
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"gooddata-sdk~=1.69.
|
|
14
|
+
"gooddata-sdk~=1.69.1.dev1",
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"orjson>=3.9.15,<4.0.0",
|
|
17
17
|
"pydantic>=2.6,<3.0",
|
|
@@ -45,6 +45,7 @@ dev = [
|
|
|
45
45
|
test = [
|
|
46
46
|
"pytest~=8.3.4",
|
|
47
47
|
"pytest-cov~=6.0.0",
|
|
48
|
+
"pytest-json-report==1.5.0",
|
|
48
49
|
"pytest-mock>=3.14.0",
|
|
49
50
|
]
|
|
50
51
|
|
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import time
|
|
7
7
|
from typing import Any, TypedDict
|
|
8
8
|
|
|
9
|
-
from gooddata_eval.core.agentic._langfuse import
|
|
9
|
+
from gooddata_eval.core.agentic._langfuse import make_langfuse_client
|
|
10
10
|
from gooddata_eval.core.agentic.alert_skill import evaluate_agentic_alert_skill
|
|
11
11
|
from gooddata_eval.core.agentic.conversation import ConversationFixture, evaluate_agentic_conversation
|
|
12
12
|
from gooddata_eval.core.agentic.general_question import evaluate_agentic_general_question
|
|
@@ -17,17 +17,14 @@ from gooddata_eval.core.agentic.visualization import evaluate_agentic_visualizat
|
|
|
17
17
|
from gooddata_eval.core.models import CreatedVisualization, DatasetItem
|
|
18
18
|
from gooddata_eval.core.runner import EvalReport, ItemReport
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
total=False,
|
|
30
|
-
)
|
|
20
|
+
|
|
21
|
+
class _LfKw(TypedDict, total=False):
|
|
22
|
+
langfuse: Any
|
|
23
|
+
dataset_item_id: str
|
|
24
|
+
dataset_name: str
|
|
25
|
+
run_timestamp: str
|
|
26
|
+
model_version_override: str | None
|
|
27
|
+
|
|
31
28
|
|
|
32
29
|
AGENTIC_TEST_KINDS = frozenset(
|
|
33
30
|
{
|
|
@@ -12,6 +12,7 @@ from gooddata_api_client.exceptions import ApiException
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
|
+
from gooddata_eval.cli.agentic_runner import AGENTIC_TEST_KINDS, run_agentic_items
|
|
15
16
|
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
16
17
|
from gooddata_eval.core.config import RunConfig
|
|
17
18
|
from gooddata_eval.core.connection import ConnectionError_, resolve_connection
|
|
@@ -20,7 +21,6 @@ from gooddata_eval.core.langfuse.sink import LangfuseSink
|
|
|
20
21
|
from gooddata_eval.core.models import ChatResult, DatasetItem
|
|
21
22
|
from gooddata_eval.core.reporting.console import render_comparison, render_console
|
|
22
23
|
from gooddata_eval.core.reporting.json_report import write_multi_model_report
|
|
23
|
-
from gooddata_eval.cli.agentic_runner import AGENTIC_TEST_KINDS, run_agentic_items
|
|
24
24
|
from gooddata_eval.core.runner import ItemReport, run_items
|
|
25
25
|
from gooddata_eval.core.summary.http_client import SummaryClient
|
|
26
26
|
from gooddata_eval.core.workspace import ModelResolutionError, WorkspaceModelController
|
|
@@ -98,6 +98,12 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
98
98
|
)
|
|
99
99
|
run.add_argument("--json", dest="json_path", help="Write a JSON report to this path.")
|
|
100
100
|
run.add_argument("--quiet", action="store_true", help="Suppress per-item progress output.")
|
|
101
|
+
run.add_argument(
|
|
102
|
+
"--preserve-failed",
|
|
103
|
+
action="store_true",
|
|
104
|
+
dest="preserve_failed",
|
|
105
|
+
help="Keep failed conversations on the server for post-mortem inspection.",
|
|
106
|
+
)
|
|
101
107
|
run.add_argument(
|
|
102
108
|
"--langfuse",
|
|
103
109
|
action="store_true",
|
|
@@ -331,7 +337,12 @@ def _run(config: RunConfig) -> int:
|
|
|
331
337
|
|
|
332
338
|
# --- non-agentic items (single-turn, use Evaluator) ---
|
|
333
339
|
backend = _RoutingBackend(
|
|
334
|
-
ChatClient(
|
|
340
|
+
ChatClient(
|
|
341
|
+
host=config.host,
|
|
342
|
+
token=config.token,
|
|
343
|
+
workspace_id=config.workspace_id,
|
|
344
|
+
preserve_failed=config.preserve_failed,
|
|
345
|
+
),
|
|
335
346
|
SummaryClient(host=config.host, token=config.token, workspace_id=config.workspace_id),
|
|
336
347
|
)
|
|
337
348
|
try:
|
|
@@ -421,6 +432,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
421
432
|
log_to_langfuse=args.langfuse,
|
|
422
433
|
quiet=args.quiet,
|
|
423
434
|
kind=args.kind,
|
|
435
|
+
preserve_failed=args.preserve_failed,
|
|
424
436
|
)
|
|
425
437
|
return _run(config)
|
|
426
438
|
except (
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/alert_skill.py
RENAMED
|
@@ -7,12 +7,10 @@ import json
|
|
|
7
7
|
import os
|
|
8
8
|
import re
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
-
|
|
11
10
|
from typing import Any
|
|
12
11
|
|
|
13
|
-
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
14
12
|
from gooddata_eval.core.agentic._catalog import CatalogMetricAlert
|
|
15
|
-
|
|
13
|
+
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
16
14
|
from gooddata_eval.core.models import ToolCallEvent
|
|
17
15
|
|
|
18
16
|
try:
|
|
@@ -80,7 +78,11 @@ def _check_trigger(expected: CatalogMetricAlert, actual_args: dict) -> bool:
|
|
|
80
78
|
if expected.operator == "ANOMALY":
|
|
81
79
|
return True
|
|
82
80
|
exp_trigger = expected.trigger
|
|
83
|
-
|
|
81
|
+
# A missing OR explicit-null trigger means "unset" -> the product persists the
|
|
82
|
+
# default ALWAYS ("Every time"). `.get(k, default)` only returns the default when
|
|
83
|
+
# the key is ABSENT, but create_metric_alert serialises unset params as
|
|
84
|
+
# `trigger: null`, so chain with `or` to also cover the present-but-None case.
|
|
85
|
+
act_trigger = actual_args.get("trigger") or actual_args.get("triggerMode") or "ALWAYS"
|
|
84
86
|
if exp_trigger in _ALWAYS_TRIGGER_VALUES:
|
|
85
87
|
return act_trigger in {"ALWAYS", "Every time"}
|
|
86
88
|
act_api = _TRIGGER_DISPLAY_TO_API.get(act_trigger, act_trigger)
|
|
@@ -438,7 +440,9 @@ def evaluate_agentic_alert_skill(
|
|
|
438
440
|
model_version_override: str | None = None,
|
|
439
441
|
) -> None:
|
|
440
442
|
"""Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure."""
|
|
441
|
-
from datetime import datetime as _dt
|
|
443
|
+
from datetime import datetime as _dt # noqa: PLC0415
|
|
444
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
445
|
+
|
|
442
446
|
from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
|
|
443
447
|
|
|
444
448
|
if langfuse is None:
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/conversation.py
RENAMED
|
@@ -386,7 +386,9 @@ def evaluate_agentic_conversation(
|
|
|
386
386
|
model_version_override: str | None = None,
|
|
387
387
|
) -> None:
|
|
388
388
|
"""Run conversation evaluation, log to Langfuse, and raise on failure."""
|
|
389
|
-
from datetime import datetime as _dt
|
|
389
|
+
from datetime import datetime as _dt # noqa: PLC0415
|
|
390
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
391
|
+
|
|
390
392
|
from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
|
|
391
393
|
|
|
392
394
|
if langfuse is None:
|
|
@@ -154,7 +154,9 @@ def evaluate_agentic_general_question(
|
|
|
154
154
|
model_version_override: str | None = None,
|
|
155
155
|
) -> None:
|
|
156
156
|
"""Run general-question evaluation, log to Langfuse, and raise on failure."""
|
|
157
|
-
from datetime import datetime as _dt
|
|
157
|
+
from datetime import datetime as _dt # noqa: PLC0415
|
|
158
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
159
|
+
|
|
158
160
|
from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
|
|
159
161
|
|
|
160
162
|
if langfuse is None:
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/guardrail.py
RENAMED
|
@@ -151,7 +151,9 @@ def evaluate_agentic_guardrail(
|
|
|
151
151
|
model_version_override: str | None = None,
|
|
152
152
|
) -> None:
|
|
153
153
|
"""Run guardrail evaluation, log to Langfuse, and raise on failure."""
|
|
154
|
-
from datetime import datetime as _dt
|
|
154
|
+
from datetime import datetime as _dt # noqa: PLC0415
|
|
155
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
156
|
+
|
|
155
157
|
from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
|
|
156
158
|
|
|
157
159
|
if langfuse is None:
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/metric_skill.py
RENAMED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
6
|
import os
|
|
9
7
|
import re
|
|
10
8
|
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
11
10
|
|
|
12
11
|
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
13
12
|
from gooddata_eval.core.models import ToolCallEvent
|
|
@@ -247,7 +246,9 @@ def evaluate_agentic_metric_skill(
|
|
|
247
246
|
model_version_override: str | None = None,
|
|
248
247
|
) -> None:
|
|
249
248
|
"""Run metric-skill evaluation, log to Langfuse, and raise MetricSkillAssertionError on failure."""
|
|
250
|
-
from datetime import datetime as _dt
|
|
249
|
+
from datetime import datetime as _dt # noqa: PLC0415
|
|
250
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
251
|
+
|
|
251
252
|
from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
|
|
252
253
|
|
|
253
254
|
if langfuse is None:
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/search_tool.py
RENAMED
|
@@ -145,7 +145,9 @@ def evaluate_agentic_search_tool(
|
|
|
145
145
|
model_version_override: str | None = None,
|
|
146
146
|
) -> None:
|
|
147
147
|
"""Run search-tool evaluation, log to Langfuse, and raise SearchToolAssertionError on failure."""
|
|
148
|
-
from datetime import datetime as _dt
|
|
148
|
+
from datetime import datetime as _dt # noqa: PLC0415
|
|
149
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
150
|
+
|
|
149
151
|
from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
|
|
150
152
|
|
|
151
153
|
if langfuse is None:
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/visualization.py
RENAMED
|
@@ -267,7 +267,9 @@ def evaluate_agentic_visualization(
|
|
|
267
267
|
) -> None:
|
|
268
268
|
"""Run visualization evaluation, log to Langfuse, and raise VisualizationAssertionError on failure."""
|
|
269
269
|
import json as _json # noqa: PLC0415
|
|
270
|
-
from datetime import datetime as _dt
|
|
270
|
+
from datetime import datetime as _dt # noqa: PLC0415
|
|
271
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
272
|
+
|
|
271
273
|
from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
|
|
272
274
|
|
|
273
275
|
if langfuse is None:
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/chat/sse_client.py
RENAMED
|
@@ -106,6 +106,7 @@ class _SseAccumulator:
|
|
|
106
106
|
call_id_to_event_index: dict[str, int] = field(default_factory=dict)
|
|
107
107
|
reasoning_steps: list[dict[str, Any]] = field(default_factory=list)
|
|
108
108
|
adhoc_viz_args: list[dict[str, Any]] = field(default_factory=list)
|
|
109
|
+
response_id: str | None = None
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
def _handle_text(content: dict[str, Any], acc: _SseAccumulator) -> None:
|
|
@@ -176,7 +177,9 @@ def _build_chat_result(acc: _SseAccumulator) -> ChatResult:
|
|
|
176
177
|
"objects": [acc.adhoc_viz_args[-1]],
|
|
177
178
|
"reasoning": "\n".join(acc.viz_reasoning_parts),
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
+
result = ChatResult.model_validate(payload)
|
|
181
|
+
result.response_id = acc.response_id
|
|
182
|
+
return result
|
|
180
183
|
|
|
181
184
|
|
|
182
185
|
def parse_sse_lines(lines: Iterable[str]) -> ChatResult:
|
|
@@ -204,9 +207,13 @@ def parse_sse_lines(lines: Iterable[str]) -> ChatResult:
|
|
|
204
207
|
if code in _RETRYABLE_STATUS_CODES:
|
|
205
208
|
raise TransientChatError(message, status_code=code, detail=detail)
|
|
206
209
|
raise ChatError(message, status_code=code, detail=detail)
|
|
210
|
+
if event_data.get("responseId") and not acc.response_id:
|
|
211
|
+
acc.response_id = event_data["responseId"]
|
|
207
212
|
item = event_data.get("item")
|
|
208
213
|
if not item:
|
|
209
214
|
continue
|
|
215
|
+
if item.get("responseId") and not acc.response_id:
|
|
216
|
+
acc.response_id = item["responseId"]
|
|
210
217
|
role = item.get("role")
|
|
211
218
|
content: dict[str, Any] = item.get("content") or {}
|
|
212
219
|
ctype = content.get("type")
|
|
@@ -227,10 +234,13 @@ def parse_sse_lines(lines: Iterable[str]) -> ChatResult:
|
|
|
227
234
|
class ChatClient:
|
|
228
235
|
"""Single-turn AI chat client over the GoodData AI conversation endpoints."""
|
|
229
236
|
|
|
230
|
-
def __init__(
|
|
237
|
+
def __init__(
|
|
238
|
+
self, host: str, token: str, workspace_id: str, *, timeout: float = 300.0, preserve_failed: bool = False
|
|
239
|
+
):
|
|
231
240
|
self._base = f"{host.rstrip('/')}/api/v1/ai/workspaces/{workspace_id}/chat/conversations"
|
|
232
241
|
self._auth = {"Authorization": f"Bearer {token}"}
|
|
233
242
|
self._client = httpx.Client(timeout=timeout)
|
|
243
|
+
self._preserve_failed = preserve_failed
|
|
234
244
|
|
|
235
245
|
def create_conversation(self) -> str:
|
|
236
246
|
def _do() -> str:
|
|
@@ -264,12 +274,29 @@ class ChatClient:
|
|
|
264
274
|
return _retry_transient(_do, is_retryable=_is_retryable_exc)
|
|
265
275
|
|
|
266
276
|
def ask(self, item: DatasetItem) -> ChatResult:
|
|
267
|
-
"""Run one
|
|
277
|
+
"""Run one conversation: create, send, parse, clean up.
|
|
278
|
+
|
|
279
|
+
The conversation_id is attached to the returned ChatResult for tracing.
|
|
280
|
+
When ``preserve_failed`` is set, failed conversations are kept on the
|
|
281
|
+
server so they can be inspected after the run; the conversation_id is
|
|
282
|
+
attached to the raised exception as well.
|
|
283
|
+
"""
|
|
268
284
|
conversation_id = self.create_conversation()
|
|
285
|
+
success = False
|
|
269
286
|
try:
|
|
270
|
-
|
|
287
|
+
result = self.send_message(conversation_id, item.question)
|
|
288
|
+
result.conversation_id = conversation_id
|
|
289
|
+
success = True
|
|
290
|
+
return result
|
|
291
|
+
except Exception as exc:
|
|
292
|
+
try:
|
|
293
|
+
object.__setattr__(exc, "conversation_id", conversation_id)
|
|
294
|
+
except TypeError:
|
|
295
|
+
pass # C-extension exception that rejects __setattr__
|
|
296
|
+
raise
|
|
271
297
|
finally:
|
|
272
|
-
self.
|
|
298
|
+
if success or not self._preserve_failed:
|
|
299
|
+
self.delete_conversation(conversation_id)
|
|
273
300
|
|
|
274
301
|
def close(self) -> None:
|
|
275
302
|
self._client.close()
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/alert_skill.py
RENAMED
|
@@ -78,7 +78,9 @@ class AlertSkillEvaluator:
|
|
|
78
78
|
|
|
79
79
|
if "Trigger" in expected:
|
|
80
80
|
expected_trigger = _TRIGGER_MAP.get(expected["Trigger"], expected["Trigger"])
|
|
81
|
-
|
|
81
|
+
# Omitted/null trigger persists as the product default ALWAYS ("Every time").
|
|
82
|
+
actual_trigger = args.get("trigger") or "ALWAYS"
|
|
83
|
+
trigger_correct = actual_trigger == expected_trigger
|
|
82
84
|
|
|
83
85
|
if "Filters" in expected:
|
|
84
86
|
actual_filters = args.get("filters") or []
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# (C) 2026 GoodData Corporation
|
|
2
|
+
"""Evaluator for search_tool: agent must call the catalog search with expected parameters."""
|
|
3
|
+
|
|
4
|
+
from gooddata_eval.core.evaluators.base import ItemEvaluation
|
|
5
|
+
from gooddata_eval.core.models import ChatResult, DatasetItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _normalize_str_list(value: object, *, lowercase: bool = False) -> list[str]:
|
|
9
|
+
# Arguments come from raw model-emitted JSON. The search_objects schema
|
|
10
|
+
# declares keywords/object_types as list[str], but a malformed tool call may
|
|
11
|
+
# send a non-list or non-string entries. Drop the offending entries defensively
|
|
12
|
+
# so bad input can't raise (.lower()/sorted() on a non-str) and abort the whole
|
|
13
|
+
# evaluation run; a non-list collapses to [] and the surviving strings are
|
|
14
|
+
# still compared normally.
|
|
15
|
+
if not isinstance(value, list):
|
|
16
|
+
return []
|
|
17
|
+
items = [item for item in value if isinstance(item, str)]
|
|
18
|
+
return sorted(item.lower() if lowercase else item for item in items)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _args_match(actual_args: dict, expected_args: dict) -> bool:
|
|
22
|
+
# Only keywords and object_types determine semantic correctness.
|
|
23
|
+
# limit is optional with a server-side default; emit_widget was renamed to
|
|
24
|
+
# user_requested_search in the tool schema — neither affects search quality.
|
|
25
|
+
actual_kw = _normalize_str_list(actual_args.get("keywords"), lowercase=True)
|
|
26
|
+
expected_kw = _normalize_str_list(expected_args.get("keywords"), lowercase=True)
|
|
27
|
+
if actual_kw != expected_kw:
|
|
28
|
+
return False
|
|
29
|
+
# object_types is compared case-sensitively (no lowercase=True): they are
|
|
30
|
+
# controlled ObjectType StrEnum values the model emits verbatim ("metric",
|
|
31
|
+
# "dashboard"), so a case mismatch is a genuine error, not a formatting quirk.
|
|
32
|
+
return _normalize_str_list(actual_args.get("object_types")) == _normalize_str_list(
|
|
33
|
+
expected_args.get("object_types")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SearchToolEvaluator:
|
|
38
|
+
test_kind = "search_tool"
|
|
39
|
+
|
|
40
|
+
def evaluate(self, item: DatasetItem, chat_result: ChatResult) -> ItemEvaluation:
|
|
41
|
+
expected_call = (item.expected_output or {}).get("tool_call", {})
|
|
42
|
+
expected_fn = expected_call.get("function_name", "search_objects")
|
|
43
|
+
expected_args = expected_call.get("function_arguments", {})
|
|
44
|
+
|
|
45
|
+
matching_events = [ev for ev in chat_result.tool_call_events if ev.function_name == expected_fn]
|
|
46
|
+
tool_selection = len(matching_events) > 0
|
|
47
|
+
tool_correctness = any(_args_match(ev.parsed_arguments(), expected_args) for ev in matching_events)
|
|
48
|
+
|
|
49
|
+
# tool_selection is the hard gate; tool_correctness is scored but not blocking
|
|
50
|
+
return ItemEvaluation(
|
|
51
|
+
passed=tool_selection,
|
|
52
|
+
rank_key=(int(tool_selection), int(tool_correctness)),
|
|
53
|
+
detail={
|
|
54
|
+
"tool_selection": tool_selection,
|
|
55
|
+
"tool_correctness": tool_correctness,
|
|
56
|
+
"expected_function": expected_fn,
|
|
57
|
+
"calls_found": len(matching_events),
|
|
58
|
+
},
|
|
59
|
+
)
|
|
@@ -94,6 +94,8 @@ class ChatResult(BaseModel):
|
|
|
94
94
|
created_visualizations: CreatedVisualizations | None = Field(default=None, alias="createdVisualizations")
|
|
95
95
|
tool_call_events: list[ToolCallEvent] = Field(default_factory=list, alias="toolCallEvents")
|
|
96
96
|
reasoning_step_count: int = Field(default=0, alias="reasoningStepCount")
|
|
97
|
+
conversation_id: str | None = Field(default=None, alias="conversationId")
|
|
98
|
+
response_id: str | None = Field(default=None, alias="responseId")
|
|
97
99
|
|
|
98
100
|
|
|
99
101
|
class SummaryInput(BaseModel):
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/json_report.py
RENAMED
|
@@ -34,6 +34,8 @@ def _build_run_dict(report: EvalReport) -> dict:
|
|
|
34
34
|
"latency_s": round(item.latency_s, 3),
|
|
35
35
|
"avg_latency_s": round(item.avg_latency_s, 3),
|
|
36
36
|
"detail": item.best_detail,
|
|
37
|
+
"conversation_id": item.conversation_id,
|
|
38
|
+
"response_id": item.response_id,
|
|
37
39
|
}
|
|
38
40
|
for item in report.items
|
|
39
41
|
},
|
|
@@ -31,6 +31,8 @@ class ItemReport:
|
|
|
31
31
|
runs: int = 0
|
|
32
32
|
latency_s: float = 0.0 # total wall-clock across this item's runs
|
|
33
33
|
best_detail: dict = field(default_factory=dict)
|
|
34
|
+
conversation_id: str | None = None
|
|
35
|
+
response_id: str | None = None
|
|
34
36
|
|
|
35
37
|
@property
|
|
36
38
|
def avg_latency_s(self) -> float:
|
|
@@ -112,6 +114,8 @@ def _run_one_item(
|
|
|
112
114
|
for run_index in range(1, runs + 1):
|
|
113
115
|
t0 = time.perf_counter()
|
|
114
116
|
chat_result = backend.ask(item)
|
|
117
|
+
report.conversation_id = getattr(chat_result, "conversation_id", None) or report.conversation_id
|
|
118
|
+
report.response_id = getattr(chat_result, "response_id", None) or report.response_id
|
|
115
119
|
evaluation = evaluator.evaluate(item, chat_result)
|
|
116
120
|
latency = time.perf_counter() - t0
|
|
117
121
|
report.runs += 1
|
|
@@ -123,7 +127,9 @@ def _run_one_item(
|
|
|
123
127
|
if on_run_done is not None:
|
|
124
128
|
on_run_done(run_index, runs, evaluation.passed, latency)
|
|
125
129
|
except Exception as e: # agent/network/parse failure for this item
|
|
126
|
-
|
|
130
|
+
conv_id = getattr(e, "conversation_id", None)
|
|
131
|
+
report.conversation_id = conv_id or report.conversation_id
|
|
132
|
+
report.error = f"{type(e).__name__}: {e}" + (f" [conversation_id={conv_id}]" if conv_id else "")
|
|
127
133
|
if best is not None:
|
|
128
134
|
report.best_detail = best.detail
|
|
129
135
|
return report
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
from unittest.mock import MagicMock, patch
|
|
4
4
|
|
|
5
5
|
from gooddata_eval.core.agentic.alert_skill import (
|
|
6
|
-
AgenticAlertSummary,
|
|
7
6
|
AlertEvaluation,
|
|
8
|
-
|
|
7
|
+
_check_trigger,
|
|
9
8
|
_deep_subset,
|
|
9
|
+
_normalize_expected_output,
|
|
10
10
|
_to_number,
|
|
11
11
|
run_agentic_alert_skill,
|
|
12
12
|
)
|
|
@@ -33,6 +33,24 @@ def test_deep_subset_missing_key():
|
|
|
33
33
|
assert _deep_subset({"a": 1, "c": 3}, {"a": 1}) is False
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def test_check_trigger_missing_or_null_defaults_to_always():
|
|
37
|
+
# "Every time" is the product default -> the agent may omit the trigger arg
|
|
38
|
+
# entirely, or serialise it as null. Both must count as ALWAYS, not a mismatch.
|
|
39
|
+
expected = _normalize_expected_output({"Operator": "GREATER_THAN", "Trigger": "Every time"})
|
|
40
|
+
assert _check_trigger(expected, {"operator": "GREATER_THAN"}) is True # key absent
|
|
41
|
+
assert _check_trigger(expected, {"trigger": None}) is True # present-but-null (the bug)
|
|
42
|
+
assert _check_trigger(expected, {"trigger": "ALWAYS"}) is True
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_check_trigger_once_needs_explicit_once():
|
|
46
|
+
# A "One time" expectation must still require an explicit ONCE - the null-default
|
|
47
|
+
# fix must not turn a wrong/absent trigger into a pass here.
|
|
48
|
+
expected = _normalize_expected_output({"Operator": "LESS_THAN", "Trigger": "One time"})
|
|
49
|
+
assert _check_trigger(expected, {"trigger": "ONCE"}) is True
|
|
50
|
+
assert _check_trigger(expected, {"trigger": None}) is False # null != ONCE
|
|
51
|
+
assert _check_trigger(expected, {"trigger": "ONCE_PER_INTERVAL"}) is False # real model error stays a fail
|
|
52
|
+
|
|
53
|
+
|
|
36
54
|
def test_alert_evaluation_strict_pass():
|
|
37
55
|
ev = AlertEvaluation(
|
|
38
56
|
alert_created=True,
|
|
@@ -4,13 +4,12 @@ from unittest.mock import MagicMock, patch
|
|
|
4
4
|
|
|
5
5
|
from gooddata_eval.core.agentic.conversation import (
|
|
6
6
|
ConversationFixture,
|
|
7
|
-
ConversationResult,
|
|
8
7
|
TurnDefinition,
|
|
9
8
|
TurnResult,
|
|
10
9
|
_resolve_refs,
|
|
11
10
|
run_agentic_conversation,
|
|
12
11
|
)
|
|
13
|
-
from gooddata_eval.core.models import
|
|
12
|
+
from gooddata_eval.core.models import ToolCallEvent
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def test_turn_definition_model():
|
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
from unittest.mock import MagicMock, patch
|
|
4
4
|
|
|
5
5
|
from gooddata_eval.core.agentic.general_question import (
|
|
6
|
-
AgenticGeneralQuestionSummary,
|
|
7
6
|
GeneralQuestionResult,
|
|
8
7
|
run_agentic_general_question,
|
|
9
8
|
)
|
|
10
|
-
from gooddata_eval.core.models import ChatResult
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
def test_general_question_result_fields():
|
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
from unittest.mock import MagicMock, patch
|
|
4
4
|
|
|
5
5
|
from gooddata_eval.core.agentic.guardrail import (
|
|
6
|
-
AgenticGuardrailSummary,
|
|
7
6
|
GuardrailResult,
|
|
8
7
|
run_agentic_guardrail,
|
|
9
8
|
)
|
|
10
|
-
from gooddata_eval.core.models import ChatResult
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
def test_guardrail_result_fields():
|
|
@@ -4,14 +4,9 @@
|
|
|
4
4
|
All tests mock ChatClient so no network is needed.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from dataclasses import dataclass
|
|
8
7
|
from unittest.mock import MagicMock, call, patch
|
|
9
8
|
|
|
10
|
-
import pytest
|
|
11
|
-
|
|
12
9
|
from gooddata_eval.core.agentic.visualization import (
|
|
13
|
-
AgenticRunSummary,
|
|
14
|
-
RunResult,
|
|
15
10
|
_execute_single_run,
|
|
16
11
|
run_agentic_visualization,
|
|
17
12
|
)
|
|
@@ -154,7 +149,8 @@ def test_run_agentic_visualization_uses_initial_conversation_for_run_0():
|
|
|
154
149
|
# create_conversation should NOT be called for run 0
|
|
155
150
|
instance.create_conversation.assert_not_called()
|
|
156
151
|
instance.send_message.assert_called_once_with("existing-conv", "Show revenue")
|
|
157
|
-
|
|
152
|
+
# the caller-supplied conversation is left intact; the function only deletes conversations it created
|
|
153
|
+
instance.delete_conversation.assert_not_called()
|
|
158
154
|
assert len(summary.run_results) == 1
|
|
159
155
|
|
|
160
156
|
|
|
@@ -176,7 +172,8 @@ def test_run_agentic_visualization_creates_fresh_conversations_for_remaining_run
|
|
|
176
172
|
)
|
|
177
173
|
|
|
178
174
|
assert instance.create_conversation.call_count == 1 # only for run 1
|
|
179
|
-
|
|
175
|
+
# only the self-created fresh-1 is deleted; the caller-supplied existing-conv is left intact
|
|
176
|
+
instance.delete_conversation.assert_called_once_with("fresh-1")
|
|
180
177
|
assert len(summary.run_results) == 2
|
|
181
178
|
|
|
182
179
|
|
|
@@ -233,7 +230,7 @@ def test_run_agentic_visualization_creates_conversation_when_no_initial_id():
|
|
|
233
230
|
instance.create_conversation.side_effect = ["new-0", "new-1"]
|
|
234
231
|
instance.send_message.return_value = _chat_with_viz()
|
|
235
232
|
|
|
236
|
-
|
|
233
|
+
run_agentic_visualization(
|
|
237
234
|
host="https://example.com",
|
|
238
235
|
token="tok",
|
|
239
236
|
workspace_id="ws",
|
|
@@ -58,6 +58,23 @@ def test_alert_evaluator_skips_absent_field():
|
|
|
58
58
|
assert result.passed is True
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def test_alert_evaluator_defaults_missing_trigger_to_always():
|
|
62
|
+
# The agent may omit `trigger` (or send null) -> backend default is ALWAYS,
|
|
63
|
+
# so an "Every time" expectation must still pass instead of a false-negative.
|
|
64
|
+
expected = {"Operator": "GREATER_THAN", "Threshold": "150", "Trigger": "Every time"}
|
|
65
|
+
ev = get_evaluator("alert_skill")
|
|
66
|
+
|
|
67
|
+
absent = ev.evaluate(_item(expected), _chat_with_alert({"operator": "GREATER_THAN", "threshold": 150}))
|
|
68
|
+
assert absent.passed is True
|
|
69
|
+
assert absent.detail["trigger_correct"] is True
|
|
70
|
+
|
|
71
|
+
null = ev.evaluate(
|
|
72
|
+
_item(expected), _chat_with_alert({"operator": "GREATER_THAN", "threshold": 150, "trigger": None})
|
|
73
|
+
)
|
|
74
|
+
assert null.passed is True
|
|
75
|
+
assert null.detail["trigger_correct"] is True
|
|
76
|
+
|
|
77
|
+
|
|
61
78
|
def test_alert_evaluator_fails_when_no_tool_call():
|
|
62
79
|
result = get_evaluator("alert_skill").evaluate(
|
|
63
80
|
_item({"Operator": "LESS_THAN"}), ChatResult.model_validate({"textResponse": "here is the alert"})
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
# (C) 2026 GoodData Corporation
|
|
2
|
+
import io
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
|
|
2
5
|
import httpx
|
|
3
6
|
import orjson
|
|
4
7
|
import pytest
|
|
@@ -86,7 +89,8 @@ def test_cli_run_end_to_end(monkeypatch, tmp_path, fixtures_dir):
|
|
|
86
89
|
]
|
|
87
90
|
)
|
|
88
91
|
assert exit_code == 0
|
|
89
|
-
|
|
92
|
+
# run keys are provider-prefixed (provider_name/model) to stay collision-free across providers
|
|
93
|
+
assert orjson.loads(out.read_bytes())["runs"]["Test Provider/gpt-5.2"]["summary"]["passed"] == 1
|
|
90
94
|
|
|
91
95
|
|
|
92
96
|
def test_cli_operational_error_exits_nonzero(monkeypatch, fixtures_dir):
|
|
@@ -405,9 +409,10 @@ def test_cli_multimodel_writes_nested_json(monkeypatch, tmp_path, fixtures_dir):
|
|
|
405
409
|
]
|
|
406
410
|
)
|
|
407
411
|
data = orjson.loads(out.read_bytes())
|
|
408
|
-
|
|
412
|
+
# keys are provider-prefixed (provider_name/model); provider_name is "P" here
|
|
413
|
+
assert data["models"] == ["P/gpt-5.2", "P/gpt-4o"]
|
|
409
414
|
assert "runs" in data and "comparison" in data
|
|
410
|
-
assert data["comparison"]["gpt-5.2"]["passed"] == 1
|
|
415
|
+
assert data["comparison"]["P/gpt-5.2"]["passed"] == 1
|
|
411
416
|
|
|
412
417
|
|
|
413
418
|
def test_cli_restore_fires_even_when_model_loop_raises(monkeypatch, fixtures_dir):
|
|
@@ -512,6 +517,62 @@ def test_cli_rejects_zero_concurrency(monkeypatch, fixtures_dir):
|
|
|
512
517
|
assert exit_code == 2
|
|
513
518
|
|
|
514
519
|
|
|
520
|
+
def test_cli_preserve_failed_flag_parsed(monkeypatch, fixtures_dir):
|
|
521
|
+
"""--preserve-failed sets preserve_failed=True in RunConfig and is passed to ChatClient."""
|
|
522
|
+
monkeypatch.setattr(cli_main, "resolve_connection", lambda host, token, profile: ("https://h", "tok"))
|
|
523
|
+
captured_kwargs: dict = {}
|
|
524
|
+
|
|
525
|
+
class _FakeController:
|
|
526
|
+
def __init__(self, *a, **k): ...
|
|
527
|
+
def get_active(self):
|
|
528
|
+
return ActiveLlmProvider(provider_id="p", default_model_id="gpt-5.2")
|
|
529
|
+
|
|
530
|
+
def resolve_and_activate(self, requested, provider=None):
|
|
531
|
+
return ResolvedModel(provider_id="p", model_id="gpt-5.2", switched=False, provider_name="P")
|
|
532
|
+
|
|
533
|
+
def restore(self, original): ...
|
|
534
|
+
def close(self): ...
|
|
535
|
+
|
|
536
|
+
monkeypatch.setattr(cli_main, "WorkspaceModelController", _FakeController)
|
|
537
|
+
|
|
538
|
+
original_chat_client = cli_main.ChatClient
|
|
539
|
+
|
|
540
|
+
def _capture_chat_client(**kwargs):
|
|
541
|
+
captured_kwargs.update(kwargs)
|
|
542
|
+
return object()
|
|
543
|
+
|
|
544
|
+
monkeypatch.setattr(cli_main, "ChatClient", _capture_chat_client)
|
|
545
|
+
|
|
546
|
+
def _fake_run(items, backend, *, runs, model, workspace_id, **kw):
|
|
547
|
+
return EvalReport(
|
|
548
|
+
model=model,
|
|
549
|
+
workspace_id=workspace_id,
|
|
550
|
+
items=[
|
|
551
|
+
ItemReport(id="i1", dataset_name="d", test_kind="visualization", question="q", pass_at_k=True, runs=1)
|
|
552
|
+
],
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
monkeypatch.setattr(cli_main, "run_items", _fake_run)
|
|
556
|
+
|
|
557
|
+
exit_code = cli_main.main(
|
|
558
|
+
[
|
|
559
|
+
"run",
|
|
560
|
+
"--host",
|
|
561
|
+
"https://h",
|
|
562
|
+
"--token",
|
|
563
|
+
"tok",
|
|
564
|
+
"--workspace",
|
|
565
|
+
"ws1",
|
|
566
|
+
"--dataset",
|
|
567
|
+
str(fixtures_dir / "sample_dataset"),
|
|
568
|
+
"--preserve-failed",
|
|
569
|
+
"--quiet",
|
|
570
|
+
]
|
|
571
|
+
)
|
|
572
|
+
assert exit_code == 0
|
|
573
|
+
assert captured_kwargs.get("preserve_failed") is True
|
|
574
|
+
|
|
575
|
+
|
|
515
576
|
def test_cli_rejects_negative_concurrency(monkeypatch, fixtures_dir):
|
|
516
577
|
monkeypatch.setattr(cli_main, "resolve_connection", lambda host, token, profile: ("https://h", "tok"))
|
|
517
578
|
exit_code = cli_main.main(
|
|
@@ -534,10 +595,6 @@ def test_cli_rejects_negative_concurrency(monkeypatch, fixtures_dir):
|
|
|
534
595
|
|
|
535
596
|
def test_progress_callbacks_thread_safe():
|
|
536
597
|
"""Verify progress callbacks can be called from multiple threads without error."""
|
|
537
|
-
import io
|
|
538
|
-
import threading
|
|
539
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
540
|
-
|
|
541
598
|
console = Console(file=io.StringIO(), force_terminal=False)
|
|
542
599
|
on_item_start, on_run_done, on_item_done = cli_main._make_progress_callbacks(console)
|
|
543
600
|
|
|
@@ -554,7 +611,9 @@ def test_progress_callbacks_thread_safe():
|
|
|
554
611
|
)
|
|
555
612
|
on_item_start(index, 100, item)
|
|
556
613
|
on_run_done(index, 100, 1, 1, index % 2 == 0, 1.5)
|
|
557
|
-
report = ItemReport(
|
|
614
|
+
report = ItemReport(
|
|
615
|
+
id=f"test-{index}", dataset_name="test", test_kind="general_question", question=f"Question {index}"
|
|
616
|
+
)
|
|
558
617
|
report.runs = 1
|
|
559
618
|
report.latency_s = 1.5
|
|
560
619
|
report.pass_at_k = index % 2 == 0
|
|
@@ -19,7 +19,8 @@ def test_item_from_raw_dict_input():
|
|
|
19
19
|
item = _item_from_raw(raw, dataset_name="ds", test_kind="visualization")
|
|
20
20
|
assert item.id == "lf-1"
|
|
21
21
|
assert item.question == "Show revenue"
|
|
22
|
-
|
|
22
|
+
# expected_output carries a "visualization" key, so _infer_test_kind classifies it as production agentic vis
|
|
23
|
+
assert item.test_kind == "vis_agentic"
|
|
23
24
|
assert item.dataset_name == "ds"
|
|
24
25
|
|
|
25
26
|
|
|
@@ -233,6 +233,31 @@ def test_run_items_concurrency_callbacks_fire_for_all_items():
|
|
|
233
233
|
assert sorted(done_ids) == [f"c{i}" for i in range(5)]
|
|
234
234
|
|
|
235
235
|
|
|
236
|
+
def test_run_items_error_report_includes_conversation_id():
|
|
237
|
+
"""When an exception has a conversation_id, it appears in the error report."""
|
|
238
|
+
|
|
239
|
+
class _ConvIdBackend:
|
|
240
|
+
def ask(self, item: DatasetItem) -> ChatResult:
|
|
241
|
+
exc = RuntimeError("agent error")
|
|
242
|
+
exc.conversation_id = "conv-xyz" # type: ignore[attr-defined]
|
|
243
|
+
raise exc
|
|
244
|
+
|
|
245
|
+
report = run_items([_item()], _ConvIdBackend(), runs=1)
|
|
246
|
+
assert report.errored == 1
|
|
247
|
+
assert "conversation_id=conv-xyz" in report.items[0].error
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_run_items_error_report_omits_conversation_id_when_absent():
|
|
251
|
+
"""When no conversation_id is set, the error string has no bracket suffix."""
|
|
252
|
+
|
|
253
|
+
class _PlainBackend:
|
|
254
|
+
def ask(self, item: DatasetItem) -> ChatResult:
|
|
255
|
+
raise RuntimeError("plain error")
|
|
256
|
+
|
|
257
|
+
report = run_items([_item()], _PlainBackend(), runs=1)
|
|
258
|
+
assert "conversation_id" not in report.items[0].error
|
|
259
|
+
|
|
260
|
+
|
|
236
261
|
def test_run_items_callback_exception_is_logged_not_swallowed(capsys):
|
|
237
262
|
"""A raising callback prints a traceback to stderr but the run continues."""
|
|
238
263
|
backend = _FakeBackend([_chat_with(_viz_obj())] * 2)
|
|
@@ -5,6 +5,7 @@ import httpx
|
|
|
5
5
|
import pytest
|
|
6
6
|
from gooddata_eval.core.chat import sse_client as sse_mod
|
|
7
7
|
from gooddata_eval.core.chat.sse_client import ChatClient, ChatError, TransientChatError, parse_sse_lines
|
|
8
|
+
from gooddata_eval.core.models import DatasetItem
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def test_parse_sse_lines_collects_text_and_visualization(fixtures_dir):
|
|
@@ -222,3 +223,120 @@ def test_float_env_uses_default_when_unset(monkeypatch):
|
|
|
222
223
|
def test_float_env_reads_override(monkeypatch):
|
|
223
224
|
monkeypatch.setenv("GD_TEST_FLOAT", "1.5")
|
|
224
225
|
assert sse_mod._float_env("GD_TEST_FLOAT", 5.0) == 1.5
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_parse_sse_lines_captures_response_id_from_event_data():
|
|
229
|
+
"""response_id is captured from the top-level event_data."""
|
|
230
|
+
lines = [
|
|
231
|
+
'data: {"responseId": "resp-123", "item": {"role": "assistant", "content": {"type": "text", "text": "hi"}}}',
|
|
232
|
+
]
|
|
233
|
+
result = parse_sse_lines(lines)
|
|
234
|
+
assert result.response_id == "resp-123"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_parse_sse_lines_captures_response_id_from_item():
|
|
238
|
+
"""response_id is captured from item when not present at top level."""
|
|
239
|
+
lines = [
|
|
240
|
+
'data: {"item": {"role": "assistant", "responseId": "resp-456", "content": {"type": "text", "text": "hi"}}}',
|
|
241
|
+
]
|
|
242
|
+
result = parse_sse_lines(lines)
|
|
243
|
+
assert result.response_id == "resp-456"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_parse_sse_lines_first_response_id_wins():
|
|
247
|
+
"""Only the first responseId encountered is kept."""
|
|
248
|
+
lines = [
|
|
249
|
+
'data: {"responseId": "first", "item": {"role": "assistant", "content": {"type": "text", "text": "a"}}}',
|
|
250
|
+
'data: {"responseId": "second", "item": {"role": "assistant", "content": {"type": "text", "text": "b"}}}',
|
|
251
|
+
]
|
|
252
|
+
result = parse_sse_lines(lines)
|
|
253
|
+
assert result.response_id == "first"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_ask_attaches_conversation_id_to_result(monkeypatch):
|
|
257
|
+
"""ChatClient.ask() sets conversation_id on the returned ChatResult."""
|
|
258
|
+
calls = []
|
|
259
|
+
|
|
260
|
+
def handler(request):
|
|
261
|
+
if request.method == "POST" and request.url.path.endswith("/conversations"):
|
|
262
|
+
return httpx.Response(200, json={"conversationId": "conv-abc"})
|
|
263
|
+
if request.method == "POST" and "messages" in str(request.url):
|
|
264
|
+
return httpx.Response(200, content=_OK_SSE)
|
|
265
|
+
if request.method == "DELETE":
|
|
266
|
+
calls.append("delete")
|
|
267
|
+
return httpx.Response(204)
|
|
268
|
+
return httpx.Response(404)
|
|
269
|
+
|
|
270
|
+
client = _client_with_handler(handler)
|
|
271
|
+
item = DatasetItem(id="t1", dataset_name="d", test_kind="visualization", question="q", expected_output={})
|
|
272
|
+
result = client.ask(item)
|
|
273
|
+
assert result.conversation_id == "conv-abc"
|
|
274
|
+
assert "delete" in calls # conversation cleaned up on success
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_ask_preserve_failed_keeps_conversation_on_error(monkeypatch):
|
|
278
|
+
"""With preserve_failed=True, failed conversations are not deleted."""
|
|
279
|
+
calls = []
|
|
280
|
+
|
|
281
|
+
def handler(request):
|
|
282
|
+
if request.method == "POST" and request.url.path.endswith("/conversations"):
|
|
283
|
+
return httpx.Response(200, json={"conversationId": "conv-fail"})
|
|
284
|
+
if request.method == "POST" and "messages" in str(request.url):
|
|
285
|
+
return httpx.Response(200, content=_NONRETRY_SSE)
|
|
286
|
+
if request.method == "DELETE":
|
|
287
|
+
calls.append("delete")
|
|
288
|
+
return httpx.Response(204)
|
|
289
|
+
return httpx.Response(404)
|
|
290
|
+
|
|
291
|
+
monkeypatch.setattr(sse_mod.time, "sleep", lambda s: None)
|
|
292
|
+
client = ChatClient(host="https://example.invalid", token="t", workspace_id="w", preserve_failed=True)
|
|
293
|
+
client._client = httpx.Client(transport=httpx.MockTransport(handler))
|
|
294
|
+
|
|
295
|
+
item = DatasetItem(id="t1", dataset_name="d", test_kind="visualization", question="q", expected_output={})
|
|
296
|
+
with pytest.raises(ChatError):
|
|
297
|
+
client.ask(item)
|
|
298
|
+
assert "delete" not in calls # conversation preserved
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_ask_without_preserve_failed_deletes_on_error(monkeypatch):
|
|
302
|
+
"""Without preserve_failed, conversations are deleted even on error."""
|
|
303
|
+
calls = []
|
|
304
|
+
|
|
305
|
+
def handler(request):
|
|
306
|
+
if request.method == "POST" and request.url.path.endswith("/conversations"):
|
|
307
|
+
return httpx.Response(200, json={"conversationId": "conv-del"})
|
|
308
|
+
if request.method == "POST" and "messages" in str(request.url):
|
|
309
|
+
return httpx.Response(200, content=_NONRETRY_SSE)
|
|
310
|
+
if request.method == "DELETE":
|
|
311
|
+
calls.append("delete")
|
|
312
|
+
return httpx.Response(204)
|
|
313
|
+
return httpx.Response(404)
|
|
314
|
+
|
|
315
|
+
monkeypatch.setattr(sse_mod.time, "sleep", lambda s: None)
|
|
316
|
+
client = _client_with_handler(handler)
|
|
317
|
+
|
|
318
|
+
item = DatasetItem(id="t1", dataset_name="d", test_kind="visualization", question="q", expected_output={})
|
|
319
|
+
with pytest.raises(ChatError):
|
|
320
|
+
client.ask(item)
|
|
321
|
+
assert "delete" in calls # conversation deleted
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_ask_attaches_conversation_id_to_exception(monkeypatch):
|
|
325
|
+
"""On failure, conversation_id is attached to the raised exception."""
|
|
326
|
+
|
|
327
|
+
def handler(request):
|
|
328
|
+
if request.method == "POST" and request.url.path.endswith("/conversations"):
|
|
329
|
+
return httpx.Response(200, json={"conversationId": "conv-exc"})
|
|
330
|
+
if request.method == "POST" and "messages" in str(request.url):
|
|
331
|
+
return httpx.Response(200, content=_NONRETRY_SSE)
|
|
332
|
+
if request.method == "DELETE":
|
|
333
|
+
return httpx.Response(204)
|
|
334
|
+
return httpx.Response(404)
|
|
335
|
+
|
|
336
|
+
monkeypatch.setattr(sse_mod.time, "sleep", lambda s: None)
|
|
337
|
+
client = _client_with_handler(handler)
|
|
338
|
+
|
|
339
|
+
item = DatasetItem(id="t1", dataset_name="d", test_kind="visualization", question="q", expected_output={})
|
|
340
|
+
with pytest.raises(ChatError) as ei:
|
|
341
|
+
client.ask(item)
|
|
342
|
+
assert ei.value.conversation_id == "conv-exc"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# (C) 2026 GoodData Corporation
|
|
2
|
+
[tox]
|
|
3
|
+
envlist = py3{10,11,12,13,14}
|
|
4
|
+
|
|
5
|
+
[testenv]
|
|
6
|
+
runner = uv-venv-lock-runner
|
|
7
|
+
package = wheel
|
|
8
|
+
wheel_build_env = .pkg
|
|
9
|
+
extras =
|
|
10
|
+
llm-judge
|
|
11
|
+
dependency_groups =
|
|
12
|
+
test
|
|
13
|
+
setenv =
|
|
14
|
+
COVERAGE_CORE=sysmon
|
|
15
|
+
commands =
|
|
16
|
+
pytest -v --cov --cov-report=xml tests {posargs} --json-report --json-report-file=.json-report-{envname}.json
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# (C) 2026 GoodData Corporation
|
|
2
|
-
"""Evaluator for search_tool: agent must call the catalog search with expected parameters."""
|
|
3
|
-
|
|
4
|
-
from gooddata_eval.core.evaluators.base import ItemEvaluation
|
|
5
|
-
from gooddata_eval.core.models import ChatResult, DatasetItem
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _args_match(actual_args: dict, expected_args: dict) -> bool:
|
|
9
|
-
if sorted(actual_args.get("keywords") or []) != sorted(expected_args.get("keywords") or []):
|
|
10
|
-
return False
|
|
11
|
-
if sorted(actual_args.get("object_types") or []) != sorted(expected_args.get("object_types") or []):
|
|
12
|
-
return False
|
|
13
|
-
if actual_args.get("limit") != expected_args.get("limit"):
|
|
14
|
-
return False
|
|
15
|
-
return actual_args.get("emit_widget") == expected_args.get("emit_widget")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class SearchToolEvaluator:
|
|
19
|
-
test_kind = "search_tool"
|
|
20
|
-
|
|
21
|
-
def evaluate(self, item: DatasetItem, chat_result: ChatResult) -> ItemEvaluation:
|
|
22
|
-
expected_call = (item.expected_output or {}).get("tool_call", {})
|
|
23
|
-
expected_fn = expected_call.get("function_name", "search_objects")
|
|
24
|
-
expected_args = expected_call.get("function_arguments", {})
|
|
25
|
-
|
|
26
|
-
matching_events = [ev for ev in chat_result.tool_call_events if ev.function_name == expected_fn]
|
|
27
|
-
tool_selection = len(matching_events) > 0
|
|
28
|
-
tool_correctness = any(_args_match(ev.parsed_arguments(), expected_args) for ev in matching_events)
|
|
29
|
-
|
|
30
|
-
# tool_selection is the hard gate; tool_correctness is scored but not blocking
|
|
31
|
-
return ItemEvaluation(
|
|
32
|
-
passed=tool_selection,
|
|
33
|
-
rank_key=(int(tool_selection), int(tool_correctness)),
|
|
34
|
-
detail={
|
|
35
|
-
"tool_selection": tool_selection,
|
|
36
|
-
"tool_correctness": tool_correctness,
|
|
37
|
-
"expected_function": expected_fn,
|
|
38
|
-
"calls_found": len(matching_events),
|
|
39
|
-
},
|
|
40
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/_catalog.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/_langfuse.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/dataset/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/dataset/langfuse_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_deep_subset.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_llm_judge.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_text_utils.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/base.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/guardrail.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/metric_skill.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/langfuse/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/console.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/summary/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/summary/http_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev1}/tests/fixtures/sse_visualization_stream.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|