gooddata-eval 1.68.1.dev3__tar.gz → 1.69.0__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 (84) hide show
  1. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/PKG-INFO +2 -2
  2. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/pyproject.toml +2 -2
  3. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/cli/main.py +16 -4
  4. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/alert_skill.py +31 -14
  5. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/conversation.py +1 -1
  6. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/scoring.py +3 -1
  7. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_cli.py +41 -0
  8. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/.gitignore +0 -0
  9. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/LICENSE.txt +0 -0
  10. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/Makefile +0 -0
  11. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/README.md +0 -0
  12. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/__init__.py +0 -0
  13. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/_version.py +0 -0
  14. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/cli/__init__.py +0 -0
  15. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/cli/agentic_runner.py +0 -0
  16. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/__init__.py +0 -0
  17. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/__init__.py +0 -0
  18. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
  19. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
  20. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/general_question.py +0 -0
  21. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/guardrail.py +0 -0
  22. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/metric_skill.py +0 -0
  23. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/search_tool.py +0 -0
  24. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/agentic/visualization.py +0 -0
  25. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/chat/__init__.py +0 -0
  26. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/chat/sse_client.py +0 -0
  27. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/config.py +0 -0
  28. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/connection.py +0 -0
  29. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/dataset/__init__.py +0 -0
  30. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
  31. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/dataset/local.py +0 -0
  32. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
  33. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
  34. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
  35. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
  36. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
  37. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/base.py +0 -0
  38. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
  39. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
  40. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
  41. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
  42. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/summary.py +0 -0
  43. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
  44. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
  45. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/langfuse/sink.py +0 -0
  46. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/models.py +0 -0
  47. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/reporting/__init__.py +0 -0
  48. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/reporting/console.py +0 -0
  49. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/reporting/json_report.py +0 -0
  50. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/runner.py +0 -0
  51. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/summary/__init__.py +0 -0
  52. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/summary/http_client.py +0 -0
  53. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/src/gooddata_eval/core/workspace.py +0 -0
  54. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/__init__.py +0 -0
  55. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/conftest.py +0 -0
  56. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
  57. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
  58. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/fixtures/sse_visualization_stream.txt +0 -0
  59. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_agentic_alert_skill.py +0 -0
  60. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_agentic_conversation.py +0 -0
  61. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_agentic_general_question.py +0 -0
  62. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_agentic_guardrail.py +0 -0
  63. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_agentic_metric_skill.py +0 -0
  64. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_agentic_search_tool.py +0 -0
  65. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_agentic_visualization.py +0 -0
  66. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_alert_skill_evaluator.py +0 -0
  67. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_connection.py +0 -0
  68. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_deep_subset.py +0 -0
  69. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_langfuse_sink.py +0 -0
  70. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_langfuse_source.py +0 -0
  71. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_llm_judge.py +0 -0
  72. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_local_loader.py +0 -0
  73. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_metric_skill_evaluator.py +0 -0
  74. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_models.py +0 -0
  75. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_reporting.py +0 -0
  76. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_runner.py +0 -0
  77. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_scoring.py +0 -0
  78. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_search_tool_evaluator.py +0 -0
  79. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_sse_client.py +0 -0
  80. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_summary_client.py +0 -0
  81. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_summary_evaluator.py +0 -0
  82. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_text_evaluators.py +0 -0
  83. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_visualization_evaluator.py +0 -0
  84. {gooddata_eval-1.68.1.dev3 → gooddata_eval-1.69.0}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gooddata-eval
3
- Version: 1.68.1.dev3
3
+ Version: 1.69.0
4
4
  Summary: Evaluate the GoodData AI agent against your own questions and models.
5
5
  Project-URL: Source, https://github.com/gooddata/gooddata-python-sdk
6
6
  Author-email: GoodData <support@gooddata.com>
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering
17
17
  Classifier: Topic :: Software Development
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
- Requires-Dist: gooddata-sdk~=1.68.1.dev3
20
+ Requires-Dist: gooddata-sdk~=1.69.0
21
21
  Requires-Dist: httpx<1.0,>=0.27
22
22
  Requires-Dist: orjson<4.0.0,>=3.9.15
23
23
  Requires-Dist: pydantic<3.0,>=2.6
@@ -1,7 +1,7 @@
1
1
  # (C) 2026 GoodData Corporation
2
2
  [project]
3
3
  name = "gooddata-eval"
4
- version = "1.68.1.dev3"
4
+ version = "1.69.0"
5
5
  description = "Evaluate the GoodData AI agent against your own questions and models."
6
6
  readme = "README.md"
7
7
  license = "MIT"
@@ -11,7 +11,7 @@ authors = [
11
11
  keywords = ["gooddata", "ai", "evaluation", "llm", "analytics", "cli"]
12
12
  requires-python = ">=3.10"
13
13
  dependencies = [
14
- "gooddata-sdk~=1.68.1.dev3",
14
+ "gooddata-sdk~=1.69.0",
15
15
  "httpx>=0.27,<1.0",
16
16
  "orjson>=3.9.15,<4.0.0",
17
17
  "pydantic>=2.6,<3.0",
@@ -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
 
@@ -139,14 +140,24 @@ def _parse_model_arg(val: str) -> tuple[str | None, str]:
139
140
 
140
141
 
141
142
  def _make_progress_callbacks(console: Console):
142
- """Build (on_item_start, on_run_done, on_item_done) callbacks that stream progress."""
143
+ """Build (on_item_start, on_run_done, on_item_done) callbacks that stream progress.
144
+
145
+ A threading lock guards all console.print() calls so that concurrent
146
+ ``--concurrency 2+`` workers do not deadlock when stdout is piped
147
+ (e.g. running in a background process).
148
+ """
149
+ _print_lock = threading.Lock()
143
150
 
144
151
  def on_item_start(index: int, total: int, item: DatasetItem) -> None:
145
- console.print(f"[dim]\\[{index}/{total}][/dim] [cyan]{item.id}[/cyan] {_truncate(item.question)}")
152
+ with _print_lock:
153
+ console.print(f"[dim]\\[{index}/{total}][/dim] [cyan]{item.id}[/cyan] {_truncate(item.question)}")
146
154
 
147
155
  def on_run_done(index: int, total: int, run_index: int, runs: int, passed: bool, latency: float) -> None:
148
156
  tag = "[green]pass[/green]" if passed else "[red]fail[/red]"
149
- console.print(f"[dim]\\[{index}/{total}][/dim] run {run_index}/{runs} {tag} [dim]{latency:.2f}s[/dim]")
157
+ with _print_lock:
158
+ console.print(
159
+ f"[dim]\\[{index}/{total}][/dim] run {run_index}/{runs} {tag} [dim]{latency:.2f}s[/dim]"
160
+ )
150
161
 
151
162
  def on_item_done(index: int, total: int, report: ItemReport) -> None:
152
163
  if report.skipped:
@@ -165,7 +176,8 @@ def _make_progress_callbacks(console: Console):
165
176
  f" [dim]({report.latency_s:.2f}s total, {report.avg_latency_s:.2f}s avg, "
166
177
  f"quality={quality_str}, {report.runs} run(s))[/dim]"
167
178
  )
168
- console.print(f"[dim]\\[{index}/{total}][/dim] -> {tag} [cyan]{report.id}[/cyan]{suffix}")
179
+ with _print_lock:
180
+ console.print(f"[dim]\\[{index}/{total}][/dim] -> {tag} [cyan]{report.id}[/cyan]{suffix}")
169
181
 
170
182
  return on_item_start, on_run_done, on_item_done
171
183
 
@@ -77,6 +77,8 @@ def _check_threshold(expected: CatalogMetricAlert, actual_args: dict) -> bool:
77
77
 
78
78
 
79
79
  def _check_trigger(expected: CatalogMetricAlert, actual_args: dict) -> bool:
80
+ if expected.operator == "ANOMALY":
81
+ return True
80
82
  exp_trigger = expected.trigger
81
83
  act_trigger = actual_args.get("trigger", actual_args.get("triggerMode", "ALWAYS"))
82
84
  if exp_trigger in _ALWAYS_TRIGGER_VALUES:
@@ -235,27 +237,42 @@ class AgenticAlertSummary:
235
237
  best: AlertRunResult
236
238
 
237
239
 
240
+ def _case_insensitive_get(d: dict, *keys: str) -> Any:
241
+ """Look up a value by key, preferring an exact match then a case-insensitive one."""
242
+ for k in keys:
243
+ if k in d:
244
+ return d[k]
245
+ lowered = {str(k).lower(): v for k, v in d.items()}
246
+ for k in keys:
247
+ if k.lower() in lowered:
248
+ return lowered[k.lower()]
249
+ return None
250
+
251
+
238
252
  def _normalize_expected_output(expected: dict) -> CatalogMetricAlert:
239
253
  """Parse expected_output dict into CatalogMetricAlert, accepting display-format or internal-format keys."""
240
- operator = expected.get("operator") or expected.get("Operator") or "GREATER_THAN"
241
- threshold = expected.get("threshold") or expected.get("Threshold")
242
- threshold_from = expected.get("threshold_from")
243
- threshold_to = expected.get("threshold_to")
244
- trigger = expected.get("trigger") or expected.get("Trigger") or "not specified"
245
-
246
- metric_id = expected.get("metric_id")
247
- if not metric_id and "Metric" in expected:
248
- m = re.search(r"\(([^)]+)\)\s*$", str(expected["Metric"]))
249
- if m:
250
- metric_id = m.group(1).strip()
251
-
252
- raw_recip = expected.get("recipients") or expected.get("Recipient(s)") or []
254
+ operator = _case_insensitive_get(expected, "operator") or "GREATER_THAN"
255
+ threshold = _case_insensitive_get(expected, "threshold")
256
+ threshold_from = _case_insensitive_get(expected, "threshold_from")
257
+ threshold_to = _case_insensitive_get(expected, "threshold_to")
258
+
259
+ trigger = _case_insensitive_get(expected, "trigger") or "not specified"
260
+ trigger = _TRIGGER_DISPLAY_TO_API.get(trigger, trigger)
261
+
262
+ metric_id = _case_insensitive_get(expected, "metric_id")
263
+ if not metric_id:
264
+ metric_disp = _case_insensitive_get(expected, "metric")
265
+ if metric_disp:
266
+ m = re.search(r"\(([^)]+)\)\s*$", str(metric_disp))
267
+ metric_id = m.group(1).strip() if m else None
268
+
269
+ raw_recip = _case_insensitive_get(expected, "recipients", "recipient(s)") or []
253
270
  if isinstance(raw_recip, str):
254
271
  recipients = [r.strip() for r in raw_recip.replace(";", ",").split(",") if r.strip()]
255
272
  else:
256
273
  recipients = list(raw_recip)
257
274
 
258
- filters = expected.get("filters") or expected.get("Time window/Filters")
275
+ filters = _case_insensitive_get(expected, "filters")
259
276
  if isinstance(filters, str) and any(kw in filters for kw in ("None", "All time")):
260
277
  filters = None
261
278
 
@@ -102,7 +102,7 @@ def _activated_skills(tool_call_events: list[ToolCallEvent]) -> list[str]:
102
102
  if tc.function_name != "set_skills":
103
103
  continue
104
104
  args = tc.parsed_arguments() or {}
105
- skills.extend(args.get("skills", []))
105
+ skills.extend(args.get("skill_names") or args.get("skills") or [])
106
106
  return list(set(skills))
107
107
 
108
108
 
@@ -71,7 +71,9 @@ def validate_cross_references(viz: CreatedVisualization) -> tuple[bool, list[str
71
71
  continue
72
72
  using_val = filter_dict.get("using", "")
73
73
  using_uri = _resolve_alias_to_uri(using_val, fields)
74
- if not using_uri.startswith(("metric/", "fact/")):
74
+ field_def = fields.get(using_val)
75
+ is_adhoc_agg = isinstance(field_def, AacQueryField) and bool(field_def.aggregation)
76
+ if not using_uri.startswith(("metric/", "fact/")) and not is_adhoc_agg:
75
77
  errors.append(
76
78
  f"ranking filter '{filter_key}': using='{using_val}' "
77
79
  f"resolves to '{using_uri}' — expected a metric/ or fact/ URI"
@@ -530,3 +530,44 @@ def test_cli_rejects_negative_concurrency(monkeypatch, fixtures_dir):
530
530
  ]
531
531
  )
532
532
  assert exit_code == 2
533
+
534
+
535
+ def test_progress_callbacks_thread_safe():
536
+ """Verify progress callbacks can be called from multiple threads without error."""
537
+ import io
538
+ import threading
539
+ from concurrent.futures import ThreadPoolExecutor, as_completed
540
+
541
+ console = Console(file=io.StringIO(), force_terminal=False)
542
+ on_item_start, on_run_done, on_item_done = cli_main._make_progress_callbacks(console)
543
+
544
+ errors: list[Exception] = []
545
+
546
+ def _worker(index: int) -> None:
547
+ try:
548
+ item = DatasetItem(
549
+ id=f"test-{index}",
550
+ dataset_name="test",
551
+ test_kind="general_question",
552
+ question=f"Question {index}",
553
+ expected_output="answer",
554
+ )
555
+ on_item_start(index, 100, item)
556
+ on_run_done(index, 100, 1, 1, index % 2 == 0, 1.5)
557
+ report = ItemReport(id=f"test-{index}", dataset_name="test", test_kind="general_question")
558
+ report.runs = 1
559
+ report.latency_s = 1.5
560
+ report.pass_at_k = index % 2 == 0
561
+ on_item_done(index, 100, report)
562
+ except Exception as e:
563
+ errors.append(e)
564
+
565
+ with ThreadPoolExecutor(max_workers=8) as pool:
566
+ futures = [pool.submit(_worker, i) for i in range(50)]
567
+ for f in as_completed(futures):
568
+ f.result() # re-raise if any thread failed
569
+
570
+ assert not errors, f"Thread-safety violation: {errors}"
571
+ output = console.file.getvalue()
572
+ assert "test-1" in output
573
+ assert "test-49" in output