gooddata-eval 1.69.1.dev1__tar.gz → 1.69.1.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/PKG-INFO +2 -2
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/pyproject.toml +2 -2
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/_langfuse.py +20 -3
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/alert_skill.py +13 -5
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/conversation.py +16 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/general_question.py +2 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/guardrail.py +2 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/metric_skill.py +84 -27
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/search_tool.py +2 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/visualization.py +2 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_conversation.py +122 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_metric_skill.py +80 -0
- gooddata_eval-1.69.1.dev2/tests/test_agentic_run_context.py +32 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/.gitignore +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/LICENSE.txt +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/Makefile +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/README.md +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/cli/agentic_runner.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/cli/main.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/chat/sse_client.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/config.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/summary.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/models.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/reporting/console.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/reporting/json_report.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/runner.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/scoring.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/summary/http_client.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/__init__.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/conftest.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_alert_skill.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_general_question.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_guardrail.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_search_tool.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_visualization.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_alert_skill_evaluator.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_cli.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_connection.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_langfuse_source.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_models.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_reporting.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_runner.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_sse_client.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_summary_client.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_visualization_evaluator.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_workspace.py +0 -0
- {gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.69.1.
|
|
3
|
+
Version: 1.69.1.dev2
|
|
4
4
|
Summary: Evaluate the GoodData AI agent against your own questions and models.
|
|
5
5
|
Project-URL: Source, https://github.com/gooddata/gooddata-python-sdk
|
|
6
6
|
Author-email: GoodData <support@gooddata.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
17
17
|
Classifier: Topic :: Software Development
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist: gooddata-sdk~=1.69.1.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.69.1.dev2
|
|
21
21
|
Requires-Dist: httpx<1.0,>=0.27
|
|
22
22
|
Requires-Dist: orjson<4.0.0,>=3.9.15
|
|
23
23
|
Requires-Dist: pydantic<3.0,>=2.6
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# (C) 2026 GoodData Corporation
|
|
2
2
|
[project]
|
|
3
3
|
name = "gooddata-eval"
|
|
4
|
-
version = "1.69.1.
|
|
4
|
+
version = "1.69.1.dev2"
|
|
5
5
|
description = "Evaluate the GoodData AI agent against your own questions and models."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
keywords = ["gooddata", "ai", "evaluation", "llm", "analytics", "cli"]
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"gooddata-sdk~=1.69.1.
|
|
14
|
+
"gooddata-sdk~=1.69.1.dev2",
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"orjson>=3.9.15,<4.0.0",
|
|
17
17
|
"pydantic>=2.6,<3.0",
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/_langfuse.py
RENAMED
|
@@ -305,7 +305,7 @@ def observe(
|
|
|
305
305
|
run_name=run_name,
|
|
306
306
|
dataset_item_id=dataset_item_id,
|
|
307
307
|
trace_id=trace_id,
|
|
308
|
-
metadata=run_metadata or {
|
|
308
|
+
metadata=run_metadata or {},
|
|
309
309
|
run_description="",
|
|
310
310
|
)
|
|
311
311
|
_log.debug(
|
|
@@ -383,14 +383,31 @@ def build_run_context(
|
|
|
383
383
|
dataset_name: str,
|
|
384
384
|
run_timestamp: str | None,
|
|
385
385
|
model_version_override: str | None,
|
|
386
|
+
run_metadata_extra: dict[str, Any] | None = None,
|
|
386
387
|
) -> tuple[str, dict[str, Any]]:
|
|
387
|
-
"""Return (run_name_base, run_metadata) with model version resolved from workspace API.
|
|
388
|
+
"""Return (run_name_base, run_metadata) with model version resolved from workspace API.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
host: GoodData host URL.
|
|
392
|
+
token: API token used to resolve the workspace model version.
|
|
393
|
+
workspace_id: Workspace whose active LLM provider is read when no override is given.
|
|
394
|
+
dataset_name: Langfuse dataset name; used as the run-name prefix.
|
|
395
|
+
run_timestamp: Shared run timestamp for the run name (falls back to the current time).
|
|
396
|
+
model_version_override: Explicit model-version tag; when set it wins over the
|
|
397
|
+
workspace lookup.
|
|
398
|
+
run_metadata_extra: Optional extra key/values added to the dataset-run metadata
|
|
399
|
+
(e.g. a testing-framework tag or a CI run id for scoping). Default None keeps
|
|
400
|
+
behavior unchanged. The SDK-derived model_version is applied last and cannot
|
|
401
|
+
be overwritten by this dict.
|
|
402
|
+
"""
|
|
388
403
|
model = get_model_version(host, token, workspace_id, model_version_override)
|
|
389
404
|
ts = run_timestamp or datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
390
405
|
base = f"{dataset_name}_{ts}"
|
|
391
406
|
if model:
|
|
392
407
|
base = f"{base}_{model}"
|
|
393
|
-
|
|
408
|
+
# Caller supplies its own run tags (e.g. testing_framework); model_version is applied
|
|
409
|
+
# last so the SDK-derived value cannot be overwritten by run_metadata_extra.
|
|
410
|
+
metadata: dict[str, Any] = dict(run_metadata_extra) if run_metadata_extra else {}
|
|
394
411
|
if model:
|
|
395
412
|
metadata["model_version"] = model
|
|
396
413
|
return base, metadata
|
|
@@ -9,6 +9,8 @@ import re
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from gooddata_sdk import GoodDataSdk
|
|
13
|
+
|
|
12
14
|
from gooddata_eval.core.agentic._catalog import CatalogMetricAlert
|
|
13
15
|
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
14
16
|
from gooddata_eval.core.models import ToolCallEvent
|
|
@@ -183,11 +185,14 @@ def generate_simulated_alert_response(
|
|
|
183
185
|
return response.choices[0].message.content or ""
|
|
184
186
|
|
|
185
187
|
|
|
186
|
-
def _delete_alert(
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
def _delete_alert(sdk: GoodDataSdk, workspace_id: str, alert_id: str) -> None:
|
|
189
|
+
"""Best-effort delete of an alert (automation) created during evaluation.
|
|
190
|
+
|
|
191
|
+
Uses the GoodData SDK entities API rather than reimplementing the REST call.
|
|
192
|
+
Failures are logged, not raised.
|
|
193
|
+
"""
|
|
189
194
|
try:
|
|
190
|
-
|
|
195
|
+
sdk._client.entities_api.delete_entity_automations(workspace_id, alert_id)
|
|
191
196
|
except Exception as exc:
|
|
192
197
|
print(f"[CLEANUP] Failed to delete alert {alert_id}: {exc}")
|
|
193
198
|
|
|
@@ -327,6 +332,7 @@ def run_agentic_alert_skill(
|
|
|
327
332
|
expected = _normalize_expected_output(expected_output)
|
|
328
333
|
run_results: list[AlertRunResult] = []
|
|
329
334
|
client = ChatClient(host=host, token=token, workspace_id=workspace_id)
|
|
335
|
+
sdk = GoodDataSdk.create(host, token)
|
|
330
336
|
|
|
331
337
|
def _run_once(conv_id: str) -> AlertRunResult:
|
|
332
338
|
alert_id_to_delete: str | None = None
|
|
@@ -375,7 +381,7 @@ def run_agentic_alert_skill(
|
|
|
375
381
|
)
|
|
376
382
|
finally:
|
|
377
383
|
if alert_id_to_delete:
|
|
378
|
-
_delete_alert(
|
|
384
|
+
_delete_alert(sdk, workspace_id, alert_id_to_delete)
|
|
379
385
|
|
|
380
386
|
try:
|
|
381
387
|
conv_id_0 = initial_conversation_id if initial_conversation_id is not None else client.create_conversation()
|
|
@@ -438,6 +444,7 @@ def evaluate_agentic_alert_skill(
|
|
|
438
444
|
dataset_name: str = "alert_skill",
|
|
439
445
|
run_timestamp: str | None = None,
|
|
440
446
|
model_version_override: str | None = None,
|
|
447
|
+
run_metadata_extra: dict | None = None,
|
|
441
448
|
) -> None:
|
|
442
449
|
"""Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure."""
|
|
443
450
|
from datetime import datetime as _dt # noqa: PLC0415
|
|
@@ -475,6 +482,7 @@ def evaluate_agentic_alert_skill(
|
|
|
475
482
|
dataset_name,
|
|
476
483
|
run_timestamp,
|
|
477
484
|
model_version_override,
|
|
485
|
+
run_metadata_extra,
|
|
478
486
|
)
|
|
479
487
|
traces_by_conv = find_traces_per_conversation(
|
|
480
488
|
langfuse,
|
|
@@ -8,8 +8,10 @@ import re
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from typing import Literal
|
|
10
10
|
|
|
11
|
+
from gooddata_sdk import GoodDataSdk
|
|
11
12
|
from pydantic import BaseModel
|
|
12
13
|
|
|
14
|
+
from gooddata_eval.core.agentic.metric_skill import _delete_metric, _extract_created_metric_ids
|
|
13
15
|
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
14
16
|
from gooddata_eval.core.models import ChatResult, ToolCallEvent
|
|
15
17
|
from gooddata_eval.core.scoring import (
|
|
@@ -283,11 +285,16 @@ def run_agentic_conversation(
|
|
|
283
285
|
replies before the agent produces the expected output.
|
|
284
286
|
"""
|
|
285
287
|
client = ChatClient(host=host, token=token, workspace_id=workspace_id)
|
|
288
|
+
sdk = GoodDataSdk.create(host, token)
|
|
286
289
|
turn_results: list[TurnResult] = []
|
|
287
290
|
turn_outputs: dict[str, dict] = {}
|
|
288
291
|
total_clarification_turns = 0
|
|
289
292
|
conversation_id: str = ""
|
|
290
293
|
owns_conversation = False
|
|
294
|
+
# Metrics created during this conversation, deleted after it completes so they do
|
|
295
|
+
# not persist in the (shared) workspace and get reused by a later test. Deferred to
|
|
296
|
+
# the end — a later turn may $ref a metric an earlier turn created.
|
|
297
|
+
created_metric_ids: list[str] = []
|
|
291
298
|
|
|
292
299
|
try:
|
|
293
300
|
if initial_conversation_id is not None:
|
|
@@ -335,6 +342,11 @@ def run_agentic_conversation(
|
|
|
335
342
|
if metric_data:
|
|
336
343
|
turn_outputs[turn.turn_id] = metric_data
|
|
337
344
|
|
|
345
|
+
# Track every metric created this turn (any turn may create one) for cleanup.
|
|
346
|
+
for metric_id in _extract_created_metric_ids(all_tool_calls):
|
|
347
|
+
if metric_id not in created_metric_ids:
|
|
348
|
+
created_metric_ids.append(metric_id)
|
|
349
|
+
|
|
338
350
|
turn_results.append(
|
|
339
351
|
TurnResult(
|
|
340
352
|
turn_id=turn.turn_id,
|
|
@@ -351,6 +363,8 @@ def run_agentic_conversation(
|
|
|
351
363
|
finally:
|
|
352
364
|
if owns_conversation and conversation_id:
|
|
353
365
|
client.delete_conversation(conversation_id)
|
|
366
|
+
for metric_id in created_metric_ids:
|
|
367
|
+
_delete_metric(sdk, workspace_id, metric_id)
|
|
354
368
|
client.close()
|
|
355
369
|
|
|
356
370
|
activated_all = {skill for tr in turn_results for skill in tr.activated_skills}
|
|
@@ -384,6 +398,7 @@ def evaluate_agentic_conversation(
|
|
|
384
398
|
dataset_name: str = "conversation",
|
|
385
399
|
run_timestamp: str | None = None,
|
|
386
400
|
model_version_override: str | None = None,
|
|
401
|
+
run_metadata_extra: dict | None = None,
|
|
387
402
|
) -> None:
|
|
388
403
|
"""Run conversation evaluation, log to Langfuse, and raise on failure."""
|
|
389
404
|
from datetime import datetime as _dt # noqa: PLC0415
|
|
@@ -419,6 +434,7 @@ def evaluate_agentic_conversation(
|
|
|
419
434
|
dataset_name or fixture.dataset_name,
|
|
420
435
|
run_timestamp,
|
|
421
436
|
model_version_override,
|
|
437
|
+
run_metadata_extra,
|
|
422
438
|
)
|
|
423
439
|
traces_by_conv = find_traces_per_conversation(
|
|
424
440
|
langfuse,
|
|
@@ -152,6 +152,7 @@ def evaluate_agentic_general_question(
|
|
|
152
152
|
dataset_name: str = "general_question",
|
|
153
153
|
run_timestamp: str | None = None,
|
|
154
154
|
model_version_override: str | None = None,
|
|
155
|
+
run_metadata_extra: dict | None = None,
|
|
155
156
|
) -> None:
|
|
156
157
|
"""Run general-question evaluation, log to Langfuse, and raise on failure."""
|
|
157
158
|
from datetime import datetime as _dt # noqa: PLC0415
|
|
@@ -188,6 +189,7 @@ def evaluate_agentic_general_question(
|
|
|
188
189
|
dataset_name,
|
|
189
190
|
run_timestamp,
|
|
190
191
|
model_version_override,
|
|
192
|
+
run_metadata_extra,
|
|
191
193
|
)
|
|
192
194
|
traces_by_conv = find_traces_per_conversation(
|
|
193
195
|
langfuse,
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/guardrail.py
RENAMED
|
@@ -149,6 +149,7 @@ def evaluate_agentic_guardrail(
|
|
|
149
149
|
dataset_name: str = "guardrail",
|
|
150
150
|
run_timestamp: str | None = None,
|
|
151
151
|
model_version_override: str | None = None,
|
|
152
|
+
run_metadata_extra: dict | None = None,
|
|
152
153
|
) -> None:
|
|
153
154
|
"""Run guardrail evaluation, log to Langfuse, and raise on failure."""
|
|
154
155
|
from datetime import datetime as _dt # noqa: PLC0415
|
|
@@ -185,6 +186,7 @@ def evaluate_agentic_guardrail(
|
|
|
185
186
|
dataset_name,
|
|
186
187
|
run_timestamp,
|
|
187
188
|
model_version_override,
|
|
189
|
+
run_metadata_extra,
|
|
188
190
|
)
|
|
189
191
|
traces_by_conv = find_traces_per_conversation(
|
|
190
192
|
langfuse,
|
|
@@ -8,6 +8,8 @@ import re
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
from gooddata_sdk import GoodDataSdk
|
|
12
|
+
|
|
11
13
|
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
12
14
|
from gooddata_eval.core.models import ToolCallEvent
|
|
13
15
|
|
|
@@ -127,6 +129,41 @@ def _extract_metric_result(tool_call_events: list[ToolCallEvent]) -> dict | None
|
|
|
127
129
|
return None
|
|
128
130
|
|
|
129
131
|
|
|
132
|
+
def _extract_created_metric_ids(tool_call_events: list[ToolCallEvent]) -> list[str]:
|
|
133
|
+
"""Ids of every metric created by ``create_metric`` calls (a turn may create more than one).
|
|
134
|
+
|
|
135
|
+
Used for cleanup so no created metric leaks — unlike ``_extract_metric_result``, which
|
|
136
|
+
returns only the first result for MAQL evaluation. Shared with conversation evaluation.
|
|
137
|
+
"""
|
|
138
|
+
metric_ids: list[str] = []
|
|
139
|
+
for tc in tool_call_events:
|
|
140
|
+
if tc.function_name != "create_metric" or not tc.result:
|
|
141
|
+
continue
|
|
142
|
+
result_data = tc.parsed_result()
|
|
143
|
+
if not result_data:
|
|
144
|
+
continue
|
|
145
|
+
data = result_data.get("data", result_data)
|
|
146
|
+
metric_id = data.get("metric_id") if isinstance(data, dict) else None
|
|
147
|
+
if metric_id and metric_id not in metric_ids:
|
|
148
|
+
metric_ids.append(metric_id)
|
|
149
|
+
return metric_ids
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _delete_metric(sdk: GoodDataSdk, workspace_id: str, metric_id: str) -> None:
|
|
153
|
+
"""Delete a metric created during evaluation.
|
|
154
|
+
|
|
155
|
+
Eval runs share a persistent workspace, so a metric left behind is picked up by
|
|
156
|
+
a later test — the agent reuses it (returning ``SELECT {id}`` instead of full
|
|
157
|
+
MAQL) and the assertion fails. Deleting the created metric on the way out keeps
|
|
158
|
+
the workspace clean for the next run. Best-effort: failures are logged, not raised.
|
|
159
|
+
Mirrors ``alert_skill._delete_alert``.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
sdk._client.entities_api.delete_entity_metrics(workspace_id, metric_id)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
print(f"[CLEANUP] Failed to delete metric {metric_id}: {exc}")
|
|
165
|
+
|
|
166
|
+
|
|
130
167
|
def _is_asking_clarification(text: str) -> bool:
|
|
131
168
|
if not text:
|
|
132
169
|
return False
|
|
@@ -136,41 +173,54 @@ def _is_asking_clarification(text: str) -> bool:
|
|
|
136
173
|
|
|
137
174
|
def _execute_single_metric_run(
|
|
138
175
|
client: ChatClient,
|
|
176
|
+
sdk: GoodDataSdk,
|
|
177
|
+
workspace_id: str,
|
|
139
178
|
conversation_id: str,
|
|
140
179
|
question: str,
|
|
141
180
|
expected_outputs: list[dict],
|
|
142
181
|
max_iterations: int,
|
|
143
182
|
) -> MetricRunResult:
|
|
144
|
-
"""Drive one full multi-turn metric-skill conversation and evaluate the result.
|
|
183
|
+
"""Drive one full multi-turn metric-skill conversation and evaluate the result.
|
|
184
|
+
|
|
185
|
+
Any metric the agent creates during this run is deleted on the way out (see
|
|
186
|
+
``_delete_metric``) so it cannot leak into — and be reused by — a later test
|
|
187
|
+
sharing the workspace.
|
|
188
|
+
"""
|
|
145
189
|
primary_expected = expected_outputs[0] if expected_outputs else {}
|
|
146
190
|
metric_result: dict | None = None
|
|
191
|
+
metric_id_to_delete: str | None = None
|
|
147
192
|
turns = 0
|
|
148
193
|
current_question = question
|
|
149
194
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
try:
|
|
196
|
+
for _iteration in range(max_iterations):
|
|
197
|
+
turns += 1
|
|
198
|
+
chat_result = client.send_message(conversation_id, current_question)
|
|
199
|
+
candidate = _extract_metric_result(chat_result.tool_call_events or [])
|
|
200
|
+
if candidate is not None:
|
|
201
|
+
metric_result = candidate
|
|
202
|
+
metric_id_to_delete = candidate.get("metric_id")
|
|
203
|
+
break
|
|
204
|
+
response_text = (chat_result.text_response or "").strip()
|
|
205
|
+
if _is_asking_clarification(response_text):
|
|
206
|
+
current_question = generate_simulated_response(response_text, primary_expected)
|
|
207
|
+
else:
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
actual_maql = (metric_result or {}).get("maql", "")
|
|
211
|
+
metric_created = metric_result is not None
|
|
212
|
+
maql_correct, _ = _best_maql_match(actual_maql, expected_outputs) if metric_created else (False, "")
|
|
213
|
+
return MetricRunResult(
|
|
214
|
+
conversation_id=conversation_id,
|
|
215
|
+
metric_result=metric_result,
|
|
216
|
+
metric_created=metric_created,
|
|
217
|
+
actual_maql=actual_maql,
|
|
218
|
+
maql_correct=maql_correct,
|
|
219
|
+
total_turns=float(turns),
|
|
220
|
+
)
|
|
221
|
+
finally:
|
|
222
|
+
if metric_id_to_delete:
|
|
223
|
+
_delete_metric(sdk, workspace_id, metric_id_to_delete)
|
|
174
224
|
|
|
175
225
|
|
|
176
226
|
def run_agentic_metric_skill(
|
|
@@ -191,12 +241,15 @@ def run_agentic_metric_skill(
|
|
|
191
241
|
expected_outputs: list[dict] = expected_output if isinstance(expected_output, list) else [expected_output]
|
|
192
242
|
run_results: list[MetricRunResult] = []
|
|
193
243
|
client = ChatClient(host=host, token=token, workspace_id=workspace_id)
|
|
244
|
+
sdk = GoodDataSdk.create(host, token)
|
|
194
245
|
|
|
195
246
|
try:
|
|
196
247
|
conv_id_0 = initial_conversation_id if initial_conversation_id is not None else client.create_conversation()
|
|
197
248
|
try:
|
|
198
249
|
run_results.append(
|
|
199
|
-
_execute_single_metric_run(
|
|
250
|
+
_execute_single_metric_run(
|
|
251
|
+
client, sdk, workspace_id, conv_id_0, question, expected_outputs, max_iterations
|
|
252
|
+
)
|
|
200
253
|
)
|
|
201
254
|
finally:
|
|
202
255
|
if initial_conversation_id is None: # only delete conversations we created
|
|
@@ -206,7 +259,9 @@ def run_agentic_metric_skill(
|
|
|
206
259
|
conv_id = client.create_conversation()
|
|
207
260
|
try:
|
|
208
261
|
run_results.append(
|
|
209
|
-
_execute_single_metric_run(
|
|
262
|
+
_execute_single_metric_run(
|
|
263
|
+
client, sdk, workspace_id, conv_id, question, expected_outputs, max_iterations
|
|
264
|
+
)
|
|
210
265
|
)
|
|
211
266
|
finally:
|
|
212
267
|
client.delete_conversation(conv_id)
|
|
@@ -244,6 +299,7 @@ def evaluate_agentic_metric_skill(
|
|
|
244
299
|
dataset_name: str = "metric_skill",
|
|
245
300
|
run_timestamp: str | None = None,
|
|
246
301
|
model_version_override: str | None = None,
|
|
302
|
+
run_metadata_extra: dict | None = None,
|
|
247
303
|
) -> None:
|
|
248
304
|
"""Run metric-skill evaluation, log to Langfuse, and raise MetricSkillAssertionError on failure."""
|
|
249
305
|
from datetime import datetime as _dt # noqa: PLC0415
|
|
@@ -281,6 +337,7 @@ def evaluate_agentic_metric_skill(
|
|
|
281
337
|
dataset_name,
|
|
282
338
|
run_timestamp,
|
|
283
339
|
model_version_override,
|
|
340
|
+
run_metadata_extra,
|
|
284
341
|
)
|
|
285
342
|
traces_by_conv = find_traces_per_conversation(
|
|
286
343
|
langfuse,
|
|
@@ -143,6 +143,7 @@ def evaluate_agentic_search_tool(
|
|
|
143
143
|
dataset_name: str = "search",
|
|
144
144
|
run_timestamp: str | None = None,
|
|
145
145
|
model_version_override: str | None = None,
|
|
146
|
+
run_metadata_extra: dict | None = None,
|
|
146
147
|
) -> None:
|
|
147
148
|
"""Run search-tool evaluation, log to Langfuse, and raise SearchToolAssertionError on failure."""
|
|
148
149
|
from datetime import datetime as _dt # noqa: PLC0415
|
|
@@ -179,6 +180,7 @@ def evaluate_agentic_search_tool(
|
|
|
179
180
|
dataset_name,
|
|
180
181
|
run_timestamp,
|
|
181
182
|
model_version_override,
|
|
183
|
+
run_metadata_extra,
|
|
182
184
|
)
|
|
183
185
|
traces_by_conv = find_traces_per_conversation(
|
|
184
186
|
langfuse,
|
|
@@ -263,6 +263,7 @@ def evaluate_agentic_visualization(
|
|
|
263
263
|
dataset_name: str = "visualization",
|
|
264
264
|
run_timestamp: str | None = None,
|
|
265
265
|
model_version_override: str | None = None,
|
|
266
|
+
run_metadata_extra: dict | None = None,
|
|
266
267
|
record_output_path: str | None = None,
|
|
267
268
|
) -> None:
|
|
268
269
|
"""Run visualization evaluation, log to Langfuse, and raise VisualizationAssertionError on failure."""
|
|
@@ -302,6 +303,7 @@ def evaluate_agentic_visualization(
|
|
|
302
303
|
dataset_name,
|
|
303
304
|
run_timestamp,
|
|
304
305
|
model_version_override,
|
|
306
|
+
run_metadata_extra,
|
|
305
307
|
)
|
|
306
308
|
K = len(summary.run_results)
|
|
307
309
|
traces_by_conv = find_traces_per_conversation(
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: LicenseRef-GoodData-Enterprise
|
|
3
3
|
from unittest.mock import MagicMock, patch
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
5
6
|
from gooddata_eval.core.agentic.conversation import (
|
|
6
7
|
ConversationFixture,
|
|
7
8
|
TurnDefinition,
|
|
@@ -12,6 +13,29 @@ from gooddata_eval.core.agentic.conversation import (
|
|
|
12
13
|
from gooddata_eval.core.models import ToolCallEvent
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
def _skills_tc(*skills):
|
|
17
|
+
tc = MagicMock(spec=ToolCallEvent)
|
|
18
|
+
tc.function_name = "set_skills"
|
|
19
|
+
tc.parsed_arguments = lambda: {"skills": list(skills)}
|
|
20
|
+
return tc
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _create_metric_tc(metric_id):
|
|
24
|
+
tc = MagicMock(spec=ToolCallEvent)
|
|
25
|
+
tc.function_name = "create_metric"
|
|
26
|
+
tc.result = "{}" # truthy so cleanup collection processes it; content comes from parsed_result
|
|
27
|
+
tc.parsed_result = lambda mid=metric_id: {"data": {"metric_id": mid, "maql": "SELECT 1"}}
|
|
28
|
+
return tc
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _metric_turn_result(tool_calls):
|
|
32
|
+
r = MagicMock()
|
|
33
|
+
r.text_response = "done"
|
|
34
|
+
r.created_visualizations = None
|
|
35
|
+
r.tool_call_events = tool_calls
|
|
36
|
+
return r
|
|
37
|
+
|
|
38
|
+
|
|
15
39
|
def test_turn_definition_model():
|
|
16
40
|
t = TurnDefinition(
|
|
17
41
|
turn_id="t1",
|
|
@@ -169,3 +193,101 @@ def test_run_agentic_conversation_creates_and_deletes_conversation():
|
|
|
169
193
|
assert result.conversation_id == "new-conv"
|
|
170
194
|
mock_client.create_conversation.assert_called_once()
|
|
171
195
|
mock_client.delete_conversation.assert_called_once_with("new-conv")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_run_agentic_conversation_deletes_created_metrics():
|
|
199
|
+
mock_client = MagicMock()
|
|
200
|
+
mock_client.create_conversation.return_value = "conv-1"
|
|
201
|
+
mock_client.send_message.return_value = _metric_turn_result([_skills_tc("metric"), _create_metric_tc("foo_metric")])
|
|
202
|
+
|
|
203
|
+
fixture = ConversationFixture(
|
|
204
|
+
id="test-metric",
|
|
205
|
+
expected_skills=["metric"],
|
|
206
|
+
turns=[
|
|
207
|
+
TurnDefinition(
|
|
208
|
+
turn_id="t1",
|
|
209
|
+
message="Create a metric counting x",
|
|
210
|
+
expected_skill="metric",
|
|
211
|
+
expected_output_type="metric",
|
|
212
|
+
)
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
with (
|
|
216
|
+
patch("gooddata_eval.core.agentic.conversation.ChatClient", return_value=mock_client),
|
|
217
|
+
patch("gooddata_eval.core.agentic.conversation.GoodDataSdk") as mock_sdk_cls,
|
|
218
|
+
):
|
|
219
|
+
mock_sdk = mock_sdk_cls.create.return_value
|
|
220
|
+
run_agentic_conversation(
|
|
221
|
+
host="http://host/api/v1/actions/workspaces/ws1/ai",
|
|
222
|
+
token="tok",
|
|
223
|
+
workspace_id="ws1",
|
|
224
|
+
fixture=fixture,
|
|
225
|
+
)
|
|
226
|
+
# The metric created during the conversation is deleted after it completes, via the SDK.
|
|
227
|
+
mock_sdk._client.entities_api.delete_entity_metrics.assert_called_once_with("ws1", "foo_metric")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _two_metric_turn_fixture():
|
|
231
|
+
return ConversationFixture(
|
|
232
|
+
id="test-multi",
|
|
233
|
+
expected_skills=["metric"],
|
|
234
|
+
turns=[
|
|
235
|
+
TurnDefinition(
|
|
236
|
+
turn_id="t1", message="Create shared", expected_skill="metric", expected_output_type="metric"
|
|
237
|
+
),
|
|
238
|
+
TurnDefinition(
|
|
239
|
+
turn_id="t2", message="Create extra", expected_skill="metric", expected_output_type="metric"
|
|
240
|
+
),
|
|
241
|
+
],
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_run_agentic_conversation_deletes_every_unique_metric_across_turns():
|
|
246
|
+
mock_client = MagicMock()
|
|
247
|
+
mock_client.create_conversation.return_value = "conv-1"
|
|
248
|
+
# Turn 1 creates "shared"; turn 2 re-creates "shared" (duplicate) and adds "extra".
|
|
249
|
+
mock_client.send_message.side_effect = [
|
|
250
|
+
_metric_turn_result([_skills_tc("metric"), _create_metric_tc("shared")]),
|
|
251
|
+
_metric_turn_result([_skills_tc("metric"), _create_metric_tc("shared"), _create_metric_tc("extra")]),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
with (
|
|
255
|
+
patch("gooddata_eval.core.agentic.conversation.ChatClient", return_value=mock_client),
|
|
256
|
+
patch("gooddata_eval.core.agentic.conversation.GoodDataSdk") as mock_sdk_cls,
|
|
257
|
+
):
|
|
258
|
+
mock_sdk = mock_sdk_cls.create.return_value
|
|
259
|
+
run_agentic_conversation(
|
|
260
|
+
host="http://host/api/v1/actions/workspaces/ws1/ai",
|
|
261
|
+
token="tok",
|
|
262
|
+
workspace_id="ws1",
|
|
263
|
+
fixture=_two_metric_turn_fixture(),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Metrics from all turns are cleaned up, and each unique id is deleted exactly once.
|
|
267
|
+
deleted = sorted(c.args for c in mock_sdk._client.entities_api.delete_entity_metrics.call_args_list)
|
|
268
|
+
assert deleted == [("ws1", "extra"), ("ws1", "shared")]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_run_agentic_conversation_deletes_metrics_even_when_a_later_turn_raises():
|
|
272
|
+
mock_client = MagicMock()
|
|
273
|
+
mock_client.create_conversation.return_value = "conv-1"
|
|
274
|
+
# Turn 1 creates "m1"; turn 2 blows up mid-run — the finally must still clean up "m1".
|
|
275
|
+
mock_client.send_message.side_effect = [
|
|
276
|
+
_metric_turn_result([_skills_tc("metric"), _create_metric_tc("m1")]),
|
|
277
|
+
RuntimeError("boom"),
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
with (
|
|
281
|
+
patch("gooddata_eval.core.agentic.conversation.ChatClient", return_value=mock_client),
|
|
282
|
+
patch("gooddata_eval.core.agentic.conversation.GoodDataSdk") as mock_sdk_cls,
|
|
283
|
+
pytest.raises(RuntimeError),
|
|
284
|
+
):
|
|
285
|
+
mock_sdk = mock_sdk_cls.create.return_value
|
|
286
|
+
run_agentic_conversation(
|
|
287
|
+
host="http://host/api/v1/actions/workspaces/ws1/ai",
|
|
288
|
+
token="tok",
|
|
289
|
+
workspace_id="ws1",
|
|
290
|
+
fixture=_two_metric_turn_fixture(),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
mock_sdk._client.entities_api.delete_entity_metrics.assert_called_once_with("ws1", "m1")
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
# SPDX-License-Identifier: LicenseRef-GoodData-Enterprise
|
|
3
3
|
from unittest.mock import MagicMock, patch
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
5
6
|
from gooddata_eval.core.agentic.metric_skill import (
|
|
6
7
|
AgenticMetricSummary,
|
|
7
8
|
MetricRunResult,
|
|
9
|
+
_delete_metric,
|
|
8
10
|
_normalize_maql,
|
|
9
11
|
run_agentic_metric_skill,
|
|
10
12
|
)
|
|
@@ -144,3 +146,81 @@ def test_run_agentic_metric_skill_creates_fresh_conversations_for_remaining_runs
|
|
|
144
146
|
# Runs 1 and 2 always create fresh; run 0 uses existing-conv
|
|
145
147
|
assert mock_client.create_conversation.call_count == 2
|
|
146
148
|
assert mock_client.delete_conversation.call_count == 2
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_delete_metric_uses_sdk_entities_api():
|
|
152
|
+
sdk = MagicMock()
|
|
153
|
+
_delete_metric(sdk, "ws1", "foo_metric")
|
|
154
|
+
sdk._client.entities_api.delete_entity_metrics.assert_called_once_with("ws1", "foo_metric")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_delete_metric_swallows_failures():
|
|
158
|
+
sdk = MagicMock()
|
|
159
|
+
sdk._client.entities_api.delete_entity_metrics.side_effect = RuntimeError("500")
|
|
160
|
+
# Cleanup is best-effort — a failed delete is logged, never propagated.
|
|
161
|
+
_delete_metric(sdk, "ws1", "foo_metric")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _create_metric_chat_result(metric_id: str = "foo_metric"):
|
|
165
|
+
return ChatResult.model_validate(
|
|
166
|
+
{
|
|
167
|
+
"textResponse": "done",
|
|
168
|
+
"toolCallEvents": [
|
|
169
|
+
{
|
|
170
|
+
"functionName": "create_metric",
|
|
171
|
+
"functionArguments": "{}",
|
|
172
|
+
"result": f'{{"data": {{"maql": "SELECT {{metric/foo}}", "metric_id": "{metric_id}"}}}}',
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"reasoningStepCount": 1,
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_run_agentic_metric_skill_deletes_created_metric():
|
|
181
|
+
mock_client = MagicMock()
|
|
182
|
+
mock_client.create_conversation.return_value = "conv-1"
|
|
183
|
+
mock_client.send_message.return_value = _create_metric_chat_result()
|
|
184
|
+
with (
|
|
185
|
+
patch("gooddata_eval.core.agentic.metric_skill.ChatClient", return_value=mock_client),
|
|
186
|
+
patch("gooddata_eval.core.agentic.metric_skill.GoodDataSdk") as mock_sdk_cls,
|
|
187
|
+
):
|
|
188
|
+
mock_sdk = mock_sdk_cls.create.return_value
|
|
189
|
+
run_agentic_metric_skill(
|
|
190
|
+
host="http://host/api/v1/actions/workspaces/ws1/ai",
|
|
191
|
+
token="tok",
|
|
192
|
+
workspace_id="ws1",
|
|
193
|
+
question="Create metric foo",
|
|
194
|
+
expected_output={"maql": "SELECT {metric/foo}"},
|
|
195
|
+
k=1,
|
|
196
|
+
max_iterations=1,
|
|
197
|
+
)
|
|
198
|
+
# The metric the run created is deleted on the way out, by its exact id, via the SDK.
|
|
199
|
+
mock_sdk._client.entities_api.delete_entity_metrics.assert_called_once_with("ws1", "foo_metric")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_run_agentic_metric_skill_deletes_metric_even_when_teardown_fails():
|
|
203
|
+
# A metric is created, then conversation teardown raises; the created metric must still
|
|
204
|
+
# have been cleaned up (its deletion happens inside the per-run finally, before teardown).
|
|
205
|
+
mock_client = MagicMock()
|
|
206
|
+
mock_client.create_conversation.return_value = "conv-1"
|
|
207
|
+
mock_client.send_message.return_value = _create_metric_chat_result()
|
|
208
|
+
mock_client.delete_conversation.side_effect = RuntimeError("teardown boom")
|
|
209
|
+
|
|
210
|
+
with (
|
|
211
|
+
patch("gooddata_eval.core.agentic.metric_skill.ChatClient", return_value=mock_client),
|
|
212
|
+
patch("gooddata_eval.core.agentic.metric_skill.GoodDataSdk") as mock_sdk_cls,
|
|
213
|
+
pytest.raises(RuntimeError),
|
|
214
|
+
):
|
|
215
|
+
mock_sdk = mock_sdk_cls.create.return_value
|
|
216
|
+
run_agentic_metric_skill(
|
|
217
|
+
host="http://host/api/v1/actions/workspaces/ws1/ai",
|
|
218
|
+
token="tok",
|
|
219
|
+
workspace_id="ws1",
|
|
220
|
+
question="Create metric foo",
|
|
221
|
+
expected_output={"maql": "SELECT {metric/foo}"},
|
|
222
|
+
k=1,
|
|
223
|
+
max_iterations=1,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
mock_sdk._client.entities_api.delete_entity_metrics.assert_called_once_with("ws1", "foo_metric")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# (C) 2026 GoodData Corporation
|
|
2
|
+
from gooddata_eval.core.agentic._langfuse import build_run_context
|
|
3
|
+
|
|
4
|
+
# model_version_override short-circuits get_model_version (no workspace API call).
|
|
5
|
+
_COMMON = {
|
|
6
|
+
"host": "h",
|
|
7
|
+
"token": "t",
|
|
8
|
+
"workspace_id": "w",
|
|
9
|
+
"dataset_name": "ds",
|
|
10
|
+
"run_timestamp": "2026-07-10_00-00-00",
|
|
11
|
+
"model_version_override": "m",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_build_run_context_includes_caller_extra_keys():
|
|
16
|
+
base, metadata = build_run_context(
|
|
17
|
+
**_COMMON, run_metadata_extra={"testing_framework": "tavern-e2e", "github_run_id": "run-123"}
|
|
18
|
+
)
|
|
19
|
+
assert base == "ds_2026-07-10_00-00-00_m"
|
|
20
|
+
assert metadata["testing_framework"] == "tavern-e2e"
|
|
21
|
+
assert metadata["github_run_id"] == "run-123"
|
|
22
|
+
assert metadata["model_version"] == "m"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_build_run_context_extra_cannot_override_model_version():
|
|
26
|
+
_, metadata = build_run_context(**_COMMON, run_metadata_extra={"model_version": "hack"})
|
|
27
|
+
assert metadata["model_version"] == "m" # SDK-derived value wins
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_build_run_context_without_extra_has_only_model_version():
|
|
31
|
+
_, metadata = build_run_context(**_COMMON)
|
|
32
|
+
assert metadata == {"model_version": "m"}
|
|
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.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/cli/agentic_runner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/_catalog.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/chat/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/chat/sse_client.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/connection.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/dataset/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/dataset/local.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/langfuse/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/langfuse/sink.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/reporting/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/reporting/console.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/summary/__init__.py
RENAMED
|
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.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/fixtures/sse_visualization_stream.txt
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_general_question.py
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
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_metric_skill_evaluator.py
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
|
{gooddata_eval-1.69.1.dev1 → gooddata_eval-1.69.1.dev2}/tests/test_visualization_evaluator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|