gooddata-eval 1.69.0__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.
Files changed (87) hide show
  1. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/PKG-INFO +2 -2
  2. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/pyproject.toml +3 -2
  3. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/cli/agentic_runner.py +9 -12
  4. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/cli/main.py +14 -2
  5. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/_langfuse.py +20 -3
  6. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/alert_skill.py +22 -10
  7. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/conversation.py +19 -1
  8. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/general_question.py +5 -1
  9. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/guardrail.py +5 -1
  10. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/metric_skill.py +88 -30
  11. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/search_tool.py +5 -1
  12. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/visualization.py +5 -1
  13. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/chat/sse_client.py +32 -5
  14. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/config.py +1 -0
  15. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/alert_skill.py +3 -1
  16. gooddata_eval-1.69.1.dev2/src/gooddata_eval/core/evaluators/search_tool.py +59 -0
  17. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/models.py +2 -0
  18. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/reporting/json_report.py +2 -0
  19. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/runner.py +7 -1
  20. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_alert_skill.py +20 -2
  21. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_conversation.py +123 -2
  22. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_general_question.py +0 -2
  23. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_guardrail.py +0 -2
  24. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_metric_skill.py +80 -0
  25. gooddata_eval-1.69.1.dev2/tests/test_agentic_run_context.py +32 -0
  26. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_search_tool.py +0 -2
  27. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_agentic_visualization.py +5 -8
  28. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_alert_skill_evaluator.py +17 -0
  29. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_cli.py +67 -8
  30. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_langfuse_source.py +2 -1
  31. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_runner.py +25 -0
  32. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_sse_client.py +118 -0
  33. gooddata_eval-1.69.1.dev2/tox.ini +16 -0
  34. gooddata_eval-1.69.0/src/gooddata_eval/core/evaluators/search_tool.py +0 -40
  35. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/.gitignore +0 -0
  36. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/LICENSE.txt +0 -0
  37. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/Makefile +0 -0
  38. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/README.md +0 -0
  39. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/__init__.py +0 -0
  40. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/_version.py +0 -0
  41. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/cli/__init__.py +0 -0
  42. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/__init__.py +0 -0
  43. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/__init__.py +0 -0
  44. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
  45. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/chat/__init__.py +0 -0
  46. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/connection.py +0 -0
  47. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  48. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
  49. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/dataset/local.py +0 -0
  50. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
  51. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  52. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
  53. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  54. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/base.py +0 -0
  55. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  56. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  57. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  58. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/summary.py +0 -0
  59. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
  60. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  61. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  62. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  63. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/reporting/console.py +0 -0
  64. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/scoring.py +0 -0
  65. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/summary/__init__.py +0 -0
  66. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/summary/http_client.py +0 -0
  67. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/src/gooddata_eval/core/workspace.py +0 -0
  68. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/__init__.py +0 -0
  69. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/conftest.py +0 -0
  70. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  71. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  72. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/fixtures/sse_visualization_stream.txt +0 -0
  73. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_connection.py +0 -0
  74. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_deep_subset.py +0 -0
  75. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_langfuse_sink.py +0 -0
  76. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_llm_judge.py +0 -0
  77. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_local_loader.py +0 -0
  78. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_metric_skill_evaluator.py +0 -0
  79. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_models.py +0 -0
  80. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_reporting.py +0 -0
  81. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_scoring.py +0 -0
  82. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_search_tool_evaluator.py +0 -0
  83. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_summary_client.py +0 -0
  84. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_summary_evaluator.py +0 -0
  85. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_text_evaluators.py +0 -0
  86. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_visualization_evaluator.py +0 -0
  87. {gooddata_eval-1.69.0 → gooddata_eval-1.69.1.dev2}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gooddata-eval
3
- Version: 1.69.0
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.0
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.0"
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.0",
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",
@@ -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 HttpxLangfuseClient, make_langfuse_client
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
- _LfKw = TypedDict(
21
- "_LfKw",
22
- {
23
- "langfuse": Any,
24
- "dataset_item_id": str,
25
- "dataset_name": str,
26
- "run_timestamp": str,
27
- "model_version_override": str | None,
28
- },
29
- total=False,
30
- )
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(host=config.host, token=config.token, workspace_id=config.workspace_id),
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 (
@@ -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 {"testing_framework": "tavern-e2e"},
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
- metadata: dict[str, Any] = {"testing_framework": "tavern-e2e"}
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
@@ -7,12 +7,12 @@ 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
- from gooddata_eval.core.agentic._catalog import CatalogMetricAlert
12
+ from gooddata_sdk import GoodDataSdk
15
13
 
14
+ from gooddata_eval.core.agentic._catalog import CatalogMetricAlert
15
+ from gooddata_eval.core.chat.sse_client import ChatClient
16
16
  from gooddata_eval.core.models import ToolCallEvent
17
17
 
18
18
  try:
@@ -80,7 +80,11 @@ def _check_trigger(expected: CatalogMetricAlert, actual_args: dict) -> bool:
80
80
  if expected.operator == "ANOMALY":
81
81
  return True
82
82
  exp_trigger = expected.trigger
83
- act_trigger = actual_args.get("trigger", actual_args.get("triggerMode", "ALWAYS"))
83
+ # A missing OR explicit-null trigger means "unset" -> the product persists the
84
+ # default ALWAYS ("Every time"). `.get(k, default)` only returns the default when
85
+ # the key is ABSENT, but create_metric_alert serialises unset params as
86
+ # `trigger: null`, so chain with `or` to also cover the present-but-None case.
87
+ act_trigger = actual_args.get("trigger") or actual_args.get("triggerMode") or "ALWAYS"
84
88
  if exp_trigger in _ALWAYS_TRIGGER_VALUES:
85
89
  return act_trigger in {"ALWAYS", "Every time"}
86
90
  act_api = _TRIGGER_DISPLAY_TO_API.get(act_trigger, act_trigger)
@@ -181,11 +185,14 @@ def generate_simulated_alert_response(
181
185
  return response.choices[0].message.content or ""
182
186
 
183
187
 
184
- def _delete_alert(client: ChatClient, workspace_id: str, alert_id: str) -> None:
185
- host = str(client._base).split("/api/")[0]
186
- url = f"{host}/api/v1/entities/workspaces/{workspace_id}/automations/{alert_id}"
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
+ """
187
194
  try:
188
- client._client.delete(url, headers=client._auth)
195
+ sdk._client.entities_api.delete_entity_automations(workspace_id, alert_id)
189
196
  except Exception as exc:
190
197
  print(f"[CLEANUP] Failed to delete alert {alert_id}: {exc}")
191
198
 
@@ -325,6 +332,7 @@ def run_agentic_alert_skill(
325
332
  expected = _normalize_expected_output(expected_output)
326
333
  run_results: list[AlertRunResult] = []
327
334
  client = ChatClient(host=host, token=token, workspace_id=workspace_id)
335
+ sdk = GoodDataSdk.create(host, token)
328
336
 
329
337
  def _run_once(conv_id: str) -> AlertRunResult:
330
338
  alert_id_to_delete: str | None = None
@@ -373,7 +381,7 @@ def run_agentic_alert_skill(
373
381
  )
374
382
  finally:
375
383
  if alert_id_to_delete:
376
- _delete_alert(client, workspace_id, alert_id_to_delete)
384
+ _delete_alert(sdk, workspace_id, alert_id_to_delete)
377
385
 
378
386
  try:
379
387
  conv_id_0 = initial_conversation_id if initial_conversation_id is not None else client.create_conversation()
@@ -436,9 +444,12 @@ def evaluate_agentic_alert_skill(
436
444
  dataset_name: str = "alert_skill",
437
445
  run_timestamp: str | None = None,
438
446
  model_version_override: str | None = None,
447
+ run_metadata_extra: dict | None = None,
439
448
  ) -> None:
440
449
  """Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure."""
441
- from datetime import datetime as _dt, timezone as _tz # noqa: PLC0415
450
+ from datetime import datetime as _dt # noqa: PLC0415
451
+ from datetime import timezone as _tz # noqa: PLC0415
452
+
442
453
  from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
443
454
 
444
455
  if langfuse is None:
@@ -471,6 +482,7 @@ def evaluate_agentic_alert_skill(
471
482
  dataset_name,
472
483
  run_timestamp,
473
484
  model_version_override,
485
+ run_metadata_extra,
474
486
  )
475
487
  traces_by_conv = find_traces_per_conversation(
476
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,9 +398,12 @@ 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
- from datetime import datetime as _dt, timezone as _tz # noqa: PLC0415
404
+ from datetime import datetime as _dt # noqa: PLC0415
405
+ from datetime import timezone as _tz # noqa: PLC0415
406
+
390
407
  from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
391
408
 
392
409
  if langfuse is None:
@@ -417,6 +434,7 @@ def evaluate_agentic_conversation(
417
434
  dataset_name or fixture.dataset_name,
418
435
  run_timestamp,
419
436
  model_version_override,
437
+ run_metadata_extra,
420
438
  )
421
439
  traces_by_conv = find_traces_per_conversation(
422
440
  langfuse,
@@ -152,9 +152,12 @@ 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
- from datetime import datetime as _dt, timezone as _tz # noqa: PLC0415
158
+ from datetime import datetime as _dt # noqa: PLC0415
159
+ from datetime import timezone as _tz # noqa: PLC0415
160
+
158
161
  from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
159
162
 
160
163
  if langfuse is None:
@@ -186,6 +189,7 @@ def evaluate_agentic_general_question(
186
189
  dataset_name,
187
190
  run_timestamp,
188
191
  model_version_override,
192
+ run_metadata_extra,
189
193
  )
190
194
  traces_by_conv = find_traces_per_conversation(
191
195
  langfuse,
@@ -149,9 +149,12 @@ 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
- from datetime import datetime as _dt, timezone as _tz # noqa: PLC0415
155
+ from datetime import datetime as _dt # noqa: PLC0415
156
+ from datetime import timezone as _tz # noqa: PLC0415
157
+
155
158
  from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
156
159
 
157
160
  if langfuse is None:
@@ -183,6 +186,7 @@ def evaluate_agentic_guardrail(
183
186
  dataset_name,
184
187
  run_timestamp,
185
188
  model_version_override,
189
+ run_metadata_extra,
186
190
  )
187
191
  traces_by_conv = find_traces_per_conversation(
188
192
  langfuse,
@@ -3,11 +3,12 @@
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
10
+
11
+ from gooddata_sdk import GoodDataSdk
11
12
 
12
13
  from gooddata_eval.core.chat.sse_client import ChatClient
13
14
  from gooddata_eval.core.models import ToolCallEvent
@@ -128,6 +129,41 @@ def _extract_metric_result(tool_call_events: list[ToolCallEvent]) -> dict | None
128
129
  return None
129
130
 
130
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
+
131
167
  def _is_asking_clarification(text: str) -> bool:
132
168
  if not text:
133
169
  return False
@@ -137,41 +173,54 @@ def _is_asking_clarification(text: str) -> bool:
137
173
 
138
174
  def _execute_single_metric_run(
139
175
  client: ChatClient,
176
+ sdk: GoodDataSdk,
177
+ workspace_id: str,
140
178
  conversation_id: str,
141
179
  question: str,
142
180
  expected_outputs: list[dict],
143
181
  max_iterations: int,
144
182
  ) -> MetricRunResult:
145
- """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
+ """
146
189
  primary_expected = expected_outputs[0] if expected_outputs else {}
147
190
  metric_result: dict | None = None
191
+ metric_id_to_delete: str | None = None
148
192
  turns = 0
149
193
  current_question = question
150
194
 
151
- for _iteration in range(max_iterations):
152
- turns += 1
153
- chat_result = client.send_message(conversation_id, current_question)
154
- candidate = _extract_metric_result(chat_result.tool_call_events or [])
155
- if candidate is not None:
156
- metric_result = candidate
157
- break
158
- response_text = (chat_result.text_response or "").strip()
159
- if _is_asking_clarification(response_text):
160
- current_question = generate_simulated_response(response_text, primary_expected)
161
- else:
162
- break
163
-
164
- actual_maql = (metric_result or {}).get("maql", "")
165
- metric_created = metric_result is not None
166
- maql_correct, _ = _best_maql_match(actual_maql, expected_outputs) if metric_created else (False, "")
167
- return MetricRunResult(
168
- conversation_id=conversation_id,
169
- metric_result=metric_result,
170
- metric_created=metric_created,
171
- actual_maql=actual_maql,
172
- maql_correct=maql_correct,
173
- total_turns=float(turns),
174
- )
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)
175
224
 
176
225
 
177
226
  def run_agentic_metric_skill(
@@ -192,12 +241,15 @@ def run_agentic_metric_skill(
192
241
  expected_outputs: list[dict] = expected_output if isinstance(expected_output, list) else [expected_output]
193
242
  run_results: list[MetricRunResult] = []
194
243
  client = ChatClient(host=host, token=token, workspace_id=workspace_id)
244
+ sdk = GoodDataSdk.create(host, token)
195
245
 
196
246
  try:
197
247
  conv_id_0 = initial_conversation_id if initial_conversation_id is not None else client.create_conversation()
198
248
  try:
199
249
  run_results.append(
200
- _execute_single_metric_run(client, conv_id_0, question, expected_outputs, max_iterations)
250
+ _execute_single_metric_run(
251
+ client, sdk, workspace_id, conv_id_0, question, expected_outputs, max_iterations
252
+ )
201
253
  )
202
254
  finally:
203
255
  if initial_conversation_id is None: # only delete conversations we created
@@ -207,7 +259,9 @@ def run_agentic_metric_skill(
207
259
  conv_id = client.create_conversation()
208
260
  try:
209
261
  run_results.append(
210
- _execute_single_metric_run(client, conv_id, question, expected_outputs, max_iterations)
262
+ _execute_single_metric_run(
263
+ client, sdk, workspace_id, conv_id, question, expected_outputs, max_iterations
264
+ )
211
265
  )
212
266
  finally:
213
267
  client.delete_conversation(conv_id)
@@ -245,9 +299,12 @@ def evaluate_agentic_metric_skill(
245
299
  dataset_name: str = "metric_skill",
246
300
  run_timestamp: str | None = None,
247
301
  model_version_override: str | None = None,
302
+ run_metadata_extra: dict | None = None,
248
303
  ) -> None:
249
304
  """Run metric-skill evaluation, log to Langfuse, and raise MetricSkillAssertionError on failure."""
250
- from datetime import datetime as _dt, timezone as _tz # noqa: PLC0415
305
+ from datetime import datetime as _dt # noqa: PLC0415
306
+ from datetime import timezone as _tz # noqa: PLC0415
307
+
251
308
  from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
252
309
 
253
310
  if langfuse is None:
@@ -280,6 +337,7 @@ def evaluate_agentic_metric_skill(
280
337
  dataset_name,
281
338
  run_timestamp,
282
339
  model_version_override,
340
+ run_metadata_extra,
283
341
  )
284
342
  traces_by_conv = find_traces_per_conversation(
285
343
  langfuse,
@@ -143,9 +143,12 @@ 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
- from datetime import datetime as _dt, timezone as _tz # noqa: PLC0415
149
+ from datetime import datetime as _dt # noqa: PLC0415
150
+ from datetime import timezone as _tz # noqa: PLC0415
151
+
149
152
  from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
150
153
 
151
154
  if langfuse is None:
@@ -177,6 +180,7 @@ def evaluate_agentic_search_tool(
177
180
  dataset_name,
178
181
  run_timestamp,
179
182
  model_version_override,
183
+ run_metadata_extra,
180
184
  )
181
185
  traces_by_conv = find_traces_per_conversation(
182
186
  langfuse,
@@ -263,11 +263,14 @@ 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."""
269
270
  import json as _json # noqa: PLC0415
270
- from datetime import datetime as _dt, timezone as _tz # noqa: PLC0415
271
+ from datetime import datetime as _dt # noqa: PLC0415
272
+ from datetime import timezone as _tz # noqa: PLC0415
273
+
271
274
  from gooddata_eval.core.agentic._langfuse import try_make_langfuse_client # noqa: PLC0415
272
275
 
273
276
  if langfuse is None:
@@ -300,6 +303,7 @@ def evaluate_agentic_visualization(
300
303
  dataset_name,
301
304
  run_timestamp,
302
305
  model_version_override,
306
+ run_metadata_extra,
303
307
  )
304
308
  K = len(summary.run_results)
305
309
  traces_by_conv = find_traces_per_conversation(