gooddata-eval 1.68.1.dev4__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.68.1.dev4 → gooddata_eval-1.69.1.dev1}/PKG-INFO +2 -2
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/pyproject.toml +3 -2
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/cli/agentic_runner.py +9 -12
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/cli/main.py +30 -6
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/alert_skill.py +9 -5
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/conversation.py +3 -1
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/general_question.py +3 -1
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/guardrail.py +3 -1
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/metric_skill.py +4 -3
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/search_tool.py +3 -1
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/visualization.py +3 -1
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/chat/sse_client.py +32 -5
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/config.py +1 -0
- {gooddata_eval-1.68.1.dev4 → 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.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/models.py +2 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/json_report.py +2 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/runner.py +7 -1
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_alert_skill.py +20 -2
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_conversation.py +1 -2
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_general_question.py +0 -2
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_guardrail.py +0 -2
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_search_tool.py +0 -2
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_visualization.py +5 -8
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_alert_skill_evaluator.py +17 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_cli.py +103 -3
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_langfuse_source.py +2 -1
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_runner.py +25 -0
- {gooddata_eval-1.68.1.dev4 → 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.68.1.dev4/src/gooddata_eval/core/evaluators/search_tool.py +0 -40
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/.gitignore +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/LICENSE.txt +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/Makefile +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/README.md +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/summary.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/reporting/console.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/scoring.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/summary/http_client.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/__init__.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/conftest.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_metric_skill.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_connection.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_models.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_reporting.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_summary_client.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_visualization_evaluator.py +0 -0
- {gooddata_eval-1.68.1.dev4 → 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
{gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/src/gooddata_eval/cli/agentic_runner.py
RENAMED
|
@@ -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
|
{
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
5
|
import sys
|
|
6
|
+
import threading
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ from gooddata_api_client.exceptions import ApiException
|
|
|
11
12
|
from rich.console import Console
|
|
12
13
|
from rich.table import Table
|
|
13
14
|
|
|
15
|
+
from gooddata_eval.cli.agentic_runner import AGENTIC_TEST_KINDS, run_agentic_items
|
|
14
16
|
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
15
17
|
from gooddata_eval.core.config import RunConfig
|
|
16
18
|
from gooddata_eval.core.connection import ConnectionError_, resolve_connection
|
|
@@ -19,7 +21,6 @@ from gooddata_eval.core.langfuse.sink import LangfuseSink
|
|
|
19
21
|
from gooddata_eval.core.models import ChatResult, DatasetItem
|
|
20
22
|
from gooddata_eval.core.reporting.console import render_comparison, render_console
|
|
21
23
|
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
|
|
23
24
|
from gooddata_eval.core.runner import ItemReport, run_items
|
|
24
25
|
from gooddata_eval.core.summary.http_client import SummaryClient
|
|
25
26
|
from gooddata_eval.core.workspace import ModelResolutionError, WorkspaceModelController
|
|
@@ -97,6 +98,12 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
97
98
|
)
|
|
98
99
|
run.add_argument("--json", dest="json_path", help="Write a JSON report to this path.")
|
|
99
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
|
+
)
|
|
100
107
|
run.add_argument(
|
|
101
108
|
"--langfuse",
|
|
102
109
|
action="store_true",
|
|
@@ -139,14 +146,24 @@ def _parse_model_arg(val: str) -> tuple[str | None, str]:
|
|
|
139
146
|
|
|
140
147
|
|
|
141
148
|
def _make_progress_callbacks(console: Console):
|
|
142
|
-
"""Build (on_item_start, on_run_done, on_item_done) callbacks that stream progress.
|
|
149
|
+
"""Build (on_item_start, on_run_done, on_item_done) callbacks that stream progress.
|
|
150
|
+
|
|
151
|
+
A threading lock guards all console.print() calls so that concurrent
|
|
152
|
+
``--concurrency 2+`` workers do not deadlock when stdout is piped
|
|
153
|
+
(e.g. running in a background process).
|
|
154
|
+
"""
|
|
155
|
+
_print_lock = threading.Lock()
|
|
143
156
|
|
|
144
157
|
def on_item_start(index: int, total: int, item: DatasetItem) -> None:
|
|
145
|
-
|
|
158
|
+
with _print_lock:
|
|
159
|
+
console.print(f"[dim]\\[{index}/{total}][/dim] [cyan]{item.id}[/cyan] {_truncate(item.question)}")
|
|
146
160
|
|
|
147
161
|
def on_run_done(index: int, total: int, run_index: int, runs: int, passed: bool, latency: float) -> None:
|
|
148
162
|
tag = "[green]pass[/green]" if passed else "[red]fail[/red]"
|
|
149
|
-
|
|
163
|
+
with _print_lock:
|
|
164
|
+
console.print(
|
|
165
|
+
f"[dim]\\[{index}/{total}][/dim] run {run_index}/{runs} {tag} [dim]{latency:.2f}s[/dim]"
|
|
166
|
+
)
|
|
150
167
|
|
|
151
168
|
def on_item_done(index: int, total: int, report: ItemReport) -> None:
|
|
152
169
|
if report.skipped:
|
|
@@ -165,7 +182,8 @@ def _make_progress_callbacks(console: Console):
|
|
|
165
182
|
f" [dim]({report.latency_s:.2f}s total, {report.avg_latency_s:.2f}s avg, "
|
|
166
183
|
f"quality={quality_str}, {report.runs} run(s))[/dim]"
|
|
167
184
|
)
|
|
168
|
-
|
|
185
|
+
with _print_lock:
|
|
186
|
+
console.print(f"[dim]\\[{index}/{total}][/dim] -> {tag} [cyan]{report.id}[/cyan]{suffix}")
|
|
169
187
|
|
|
170
188
|
return on_item_start, on_run_done, on_item_done
|
|
171
189
|
|
|
@@ -319,7 +337,12 @@ def _run(config: RunConfig) -> int:
|
|
|
319
337
|
|
|
320
338
|
# --- non-agentic items (single-turn, use Evaluator) ---
|
|
321
339
|
backend = _RoutingBackend(
|
|
322
|
-
ChatClient(
|
|
340
|
+
ChatClient(
|
|
341
|
+
host=config.host,
|
|
342
|
+
token=config.token,
|
|
343
|
+
workspace_id=config.workspace_id,
|
|
344
|
+
preserve_failed=config.preserve_failed,
|
|
345
|
+
),
|
|
323
346
|
SummaryClient(host=config.host, token=config.token, workspace_id=config.workspace_id),
|
|
324
347
|
)
|
|
325
348
|
try:
|
|
@@ -409,6 +432,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
409
432
|
log_to_langfuse=args.langfuse,
|
|
410
433
|
quiet=args.quiet,
|
|
411
434
|
kind=args.kind,
|
|
435
|
+
preserve_failed=args.preserve_failed,
|
|
412
436
|
)
|
|
413
437
|
return _run(config)
|
|
414
438
|
except (
|
|
@@ -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:
|
|
@@ -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.68.1.dev4 → 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:
|
|
@@ -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:
|
|
@@ -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:
|
|
@@ -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.68.1.dev4 → 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()
|
|
@@ -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):
|
|
@@ -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():
|
{gooddata_eval-1.68.1.dev4 → gooddata_eval-1.69.1.dev1}/tests/test_agentic_general_question.py
RENAMED
|
@@ -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",
|