gooddata-eval 1.77.0__tar.gz → 1.77.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.77.0 → gooddata_eval-1.77.1.dev2}/PKG-INFO +2 -2
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/pyproject.toml +2 -2
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/alert_skill.py +124 -17
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/document_skill.py +1 -1
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_alert_skill.py +209 -1
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_document_skill.py +2 -2
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/.gitignore +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/AGENTS.md +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/CLAUDE.md +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/LICENSE.txt +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/Makefile +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/README.md +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/scripts/verify_guardrail_refusal_criteria.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/cli/agentic_runner.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/cli/main.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/_output.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_gate.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/anomaly_detection.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/conversation.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/dashboard_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/general_question.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/guardrail.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/kda_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/metric_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/search_tool.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/visualization.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/what_if.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/chat/render.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/chat/sse_client.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/config.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/summary.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/granularity.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/_env.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/client.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/observations.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/models.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/console.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/html_report.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/json_report.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/report_template.html +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/runner.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/scoring.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/summary/http_client.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/timing.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/__init__.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/_fake_langfuse.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/conftest.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_anomaly_detection.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_conversation.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_dashboard_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_gate.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_general_question.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_guardrail.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_join_trace.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_kda_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_langfuse_trace.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_metric_skill.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_observe_experiment.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_run_context.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_runner.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_search_tool.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_visualization.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_agentic_what_if.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_alert_skill_evaluator.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_chat_render.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_cli.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_connection.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_fake_langfuse.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_from_insights.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_guardrail_criteria.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_html_report.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_client.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_e2e_fake_server.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_env.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_experiment.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_item_scope.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_observations.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_otlp.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_langfuse_source.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_maql_normalize.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_models.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_reporting.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_runner.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_sse_client.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_summary_client.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_timing.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_trace_linker.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_visualization_evaluator.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/test_workspace.py +0 -0
- {gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.77.
|
|
3
|
+
Version: 1.77.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.77.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.77.1.dev2
|
|
21
21
|
Requires-Dist: httpx<1.0,>=0.27
|
|
22
22
|
Requires-Dist: jsonpatch<2.0,>=1.33
|
|
23
23
|
Requires-Dist: orjson<4.0.0,>=3.9.15
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# (C) 2026 GoodData Corporation
|
|
2
2
|
[project]
|
|
3
3
|
name = "gooddata-eval"
|
|
4
|
-
version = "1.77.
|
|
4
|
+
version = "1.77.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.77.
|
|
14
|
+
"gooddata-sdk~=1.77.1.dev2",
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"jsonpatch>=1.33,<2.0",
|
|
17
17
|
"orjson>=3.9.15,<4.0.0",
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/alert_skill.py
RENAMED
|
@@ -9,6 +9,7 @@ import re
|
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
import httpx
|
|
12
13
|
from gooddata_sdk import GoodDataSdk
|
|
13
14
|
|
|
14
15
|
from gooddata_eval.core.agentic._catalog import AnomalyDetectionGranularity, CatalogMetricAlert
|
|
@@ -54,6 +55,11 @@ _DEFAULT_MAX_ITERATIONS = 6
|
|
|
54
55
|
|
|
55
56
|
_TRIGGER_DISPLAY_TO_API = {"Every time": "ALWAYS", "One time": "ONCE"}
|
|
56
57
|
_ALWAYS_TRIGGER_VALUES = {"Every time", "ALWAYS", "not specified"}
|
|
58
|
+
_RELATIVE_OPERATORS = ("INCREASES_BY", "DECREASES_BY", "CHANGES_BY")
|
|
59
|
+
|
|
60
|
+
# Expected-recipient spellings that mean the owner of the API token.
|
|
61
|
+
_CURRENT_USER_RECIPIENT = "me"
|
|
62
|
+
_CURRENT_USER_MARKERS = {"me", "myself", "current user", "current_user"}
|
|
57
63
|
|
|
58
64
|
_TRIGGER_INSTRUCTIONS = {
|
|
59
65
|
"ALWAYS": (
|
|
@@ -242,9 +248,9 @@ def _resolve_internal_recipient_ids(sdk: GoodDataSdk, emails: list[str]) -> set[
|
|
|
242
248
|
`create_metric_alert` then addresses the alert by internal user id
|
|
243
249
|
(`internal_recipients`), never by email, so an expected email has to be
|
|
244
250
|
resolved before it can be compared against that field. Failures (no
|
|
245
|
-
matching user, no permission, network error) are
|
|
246
|
-
|
|
247
|
-
which is correct -- it doesn't mean the alert itself failed.
|
|
251
|
+
matching user, no permission, network error) are logged and return an
|
|
252
|
+
empty set: the caller treats that the same as "this delivery path doesn't
|
|
253
|
+
match", which is correct -- it doesn't mean the alert itself failed.
|
|
248
254
|
"""
|
|
249
255
|
if not emails:
|
|
250
256
|
return set()
|
|
@@ -254,12 +260,48 @@ def _resolve_internal_recipient_ids(sdk: GoodDataSdk, emails: list[str]) -> set[
|
|
|
254
260
|
escaped = [email.replace("\\", "\\\\").replace("'", "\\'") for email in emails]
|
|
255
261
|
quoted = ",".join(f"'{email}'" for email in escaped)
|
|
256
262
|
resp = sdk._client.entities_api.get_all_entities_users(filter=f"email=in=({quoted})")
|
|
257
|
-
|
|
258
|
-
except Exception:
|
|
263
|
+
ids = {u.id for u in (resp.data or [])}
|
|
264
|
+
except Exception as exc:
|
|
265
|
+
print(f"[RECIPIENTS] Looking up users by {len(emails)} recipient email(s) failed: {exc}")
|
|
259
266
|
return set()
|
|
267
|
+
if not ids:
|
|
268
|
+
print(
|
|
269
|
+
f"[RECIPIENTS] No user has any of the {len(emails)} recipient email(s); internal recipients cannot match."
|
|
270
|
+
)
|
|
271
|
+
return ids
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _resolve_current_user_id(host: str, token: str) -> str | None:
|
|
275
|
+
"""Id of the user that owns ``token``, or None if the profile cannot be read.
|
|
276
|
+
|
|
277
|
+
The current user is matched by id, not email: that user may have no email at all.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
resp = httpx.get(
|
|
281
|
+
f"{host.rstrip('/')}/api/v1/profile",
|
|
282
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
283
|
+
timeout=30.0,
|
|
284
|
+
)
|
|
285
|
+
resp.raise_for_status()
|
|
286
|
+
user_id = resp.json().get("userId")
|
|
287
|
+
except Exception as exc:
|
|
288
|
+
print(f"[RECIPIENTS] Resolving the current user failed: {exc}")
|
|
289
|
+
return None
|
|
290
|
+
return user_id if isinstance(user_id, str) and user_id else None
|
|
291
|
+
|
|
260
292
|
|
|
293
|
+
def _split_current_user(recipients: list[str]) -> tuple[bool, list[str]]:
|
|
294
|
+
"""Whether ``recipients`` names the current user, and the email recipients besides it."""
|
|
295
|
+
emails = [r for r in recipients if r != _CURRENT_USER_RECIPIENT]
|
|
296
|
+
return len(emails) != len(recipients), emails
|
|
261
297
|
|
|
262
|
-
|
|
298
|
+
|
|
299
|
+
def _check_recipients(
|
|
300
|
+
expected: CatalogMetricAlert,
|
|
301
|
+
actual_args: dict,
|
|
302
|
+
sdk: GoodDataSdk | None = None,
|
|
303
|
+
current_user_id: str | None = None,
|
|
304
|
+
) -> bool:
|
|
263
305
|
if not expected.recipients:
|
|
264
306
|
return True
|
|
265
307
|
act_recip_raw = actual_args.get("recipients", actual_args.get("external_recipients"))
|
|
@@ -274,8 +316,6 @@ def _check_recipients(expected: CatalogMetricAlert, actual_args: dict, sdk: Good
|
|
|
274
316
|
act_recip = act_recip_raw
|
|
275
317
|
else:
|
|
276
318
|
act_recip = []
|
|
277
|
-
if set(expected.recipients) == set(act_recip or []):
|
|
278
|
-
return True
|
|
279
319
|
act_internal_raw = actual_args.get("internal_recipients")
|
|
280
320
|
# internal_recipients is declared `anyOf: [array of string, string, null]` in the
|
|
281
321
|
# create_metric_alert tool schema -- a single id as a bare string is schema-legal,
|
|
@@ -287,8 +327,27 @@ def _check_recipients(expected: CatalogMetricAlert, actual_args: dict, sdk: Good
|
|
|
287
327
|
act_internal = act_internal_raw
|
|
288
328
|
else:
|
|
289
329
|
act_internal = []
|
|
330
|
+
wants_current_user, expected_emails = _split_current_user(expected.recipients)
|
|
331
|
+
if wants_current_user:
|
|
332
|
+
if current_user_id is None or current_user_id not in act_internal:
|
|
333
|
+
return False
|
|
334
|
+
other_internal = set(act_internal) - {current_user_id}
|
|
335
|
+
external = set(act_recip or [])
|
|
336
|
+
if external - set(expected_emails):
|
|
337
|
+
return False
|
|
338
|
+
# An email not sent externally must arrive as its user's id (workspace-restricted channel).
|
|
339
|
+
unmatched = [e for e in expected_emails if e not in external]
|
|
340
|
+
if not unmatched:
|
|
341
|
+
return not other_internal
|
|
342
|
+
if sdk is None or not other_internal:
|
|
343
|
+
return False
|
|
344
|
+
# One lookup per email: a bulk lookup returns only the users that exist, hiding a missing one.
|
|
345
|
+
ids_per_email = [_resolve_internal_recipient_ids(sdk, [email]) for email in unmatched]
|
|
346
|
+
return all(ids_per_email) and other_internal == set().union(*ids_per_email)
|
|
347
|
+
if set(expected_emails) == set(act_recip or []):
|
|
348
|
+
return True
|
|
290
349
|
if sdk is not None and act_internal:
|
|
291
|
-
internal_recipient_ids = _resolve_internal_recipient_ids(sdk,
|
|
350
|
+
internal_recipient_ids = _resolve_internal_recipient_ids(sdk, expected_emails)
|
|
292
351
|
if internal_recipient_ids & set(act_internal):
|
|
293
352
|
return True
|
|
294
353
|
return False
|
|
@@ -299,6 +358,7 @@ def generate_simulated_alert_response(
|
|
|
299
358
|
expected: CatalogMetricAlert,
|
|
300
359
|
conversation_history: list,
|
|
301
360
|
question: str = "",
|
|
361
|
+
current_user_id: str | None = None,
|
|
302
362
|
) -> str:
|
|
303
363
|
"""Stateful sim-user reply for alert-skill conversation (gpt-4o).
|
|
304
364
|
|
|
@@ -306,6 +366,8 @@ def generate_simulated_alert_response(
|
|
|
306
366
|
history — the opening question went straight to the agent, never to the sim-user — so
|
|
307
367
|
without it rule 5's "the filters your original request implies" refers to text the model
|
|
308
368
|
cannot see. Optional (defaults to "") to keep the signature backwards compatible.
|
|
369
|
+
|
|
370
|
+
``current_user_id`` lets the sim-user recognise itself as the recipient of a "me" alert.
|
|
309
371
|
"""
|
|
310
372
|
if _OpenAI is None:
|
|
311
373
|
raise RuntimeError(
|
|
@@ -333,7 +395,30 @@ def generate_simulated_alert_response(
|
|
|
333
395
|
threshold = expected.threshold
|
|
334
396
|
else:
|
|
335
397
|
threshold = "not specified"
|
|
336
|
-
|
|
398
|
+
wants_current_user, emails = _split_current_user(expected.recipients)
|
|
399
|
+
recipient_names = list(emails)
|
|
400
|
+
if wants_current_user:
|
|
401
|
+
user_id_note = f", user id {current_user_id}" if current_user_id else ""
|
|
402
|
+
recipient_names.insert(0, f"me (yourself, the current user{user_id_note})")
|
|
403
|
+
recipients = ", ".join(recipient_names) if recipient_names else "not specified"
|
|
404
|
+
if wants_current_user and not emails:
|
|
405
|
+
# An email given here would make the agent send the alert to it instead of the current user.
|
|
406
|
+
recipients_rule = (
|
|
407
|
+
"4. The alert is for you yourself. Never give an email address and never ask for one to "
|
|
408
|
+
"be added. A proposal whose recipient is you — shown as 'You', the current user or your "
|
|
409
|
+
"own user id — has the correct recipient.\n"
|
|
410
|
+
)
|
|
411
|
+
elif wants_current_user:
|
|
412
|
+
recipients_rule = (
|
|
413
|
+
f"4. Proactively state the email recipients in your first reply: {', '.join(emails)}, "
|
|
414
|
+
"and say the alert should also go to you yourself. A recipient shown as 'You', the "
|
|
415
|
+
"current user or your own user id is you.\n"
|
|
416
|
+
)
|
|
417
|
+
else:
|
|
418
|
+
recipients_rule = (
|
|
419
|
+
"4. Proactively include your email recipient in your first reply. "
|
|
420
|
+
" Do not wait for the agent to ask — state it alongside the metric and condition answers.\n"
|
|
421
|
+
)
|
|
337
422
|
trigger = expected.trigger
|
|
338
423
|
filters = expected.filters
|
|
339
424
|
|
|
@@ -378,14 +463,25 @@ def generate_simulated_alert_response(
|
|
|
378
463
|
# and scoring an item against an interval it never asked for is the defect this rule
|
|
379
464
|
# exists to undo.
|
|
380
465
|
granularity = (expected.granularity or AnomalyDetectionGranularity.DAY).value
|
|
381
|
-
|
|
466
|
+
period_rule = (
|
|
382
467
|
"7. This is an ANOMALY alert. Anomaly detection REQUIRES a time granularity, and that "
|
|
383
468
|
f"granularity is NOT a date filter. State it in your first reply and repeat it whenever "
|
|
384
469
|
f"asked: use {granularity} granularity. Rule 5 constrains filters on the metric only — it "
|
|
385
470
|
"never applies to this detection interval, so never refuse to give one.\n"
|
|
386
471
|
)
|
|
472
|
+
elif operator in _RELATIVE_OPERATORS:
|
|
473
|
+
# The item names no comparison date or period; delegating is an answer the agent accepts,
|
|
474
|
+
# refusing loops until the turn budget runs out.
|
|
475
|
+
period_rule = (
|
|
476
|
+
"7. This is a comparison alert: keep the comparison your original request describes. The "
|
|
477
|
+
"agent may need a comparison date and a period length (day, week, month, quarter or "
|
|
478
|
+
"year), and those are NOT a date filter. Rule 5 constrains filters on the metric only — "
|
|
479
|
+
"it never applies to the comparison. If the agent asks which date or which period the "
|
|
480
|
+
"comparison should use, do not refuse and do not name one yourself: hand the choice back, "
|
|
481
|
+
"for example 'Pick whichever date and period fit best.'\n"
|
|
482
|
+
)
|
|
387
483
|
else:
|
|
388
|
-
|
|
484
|
+
period_rule = (
|
|
389
485
|
"7. Do not invent an evaluation period, a granularity or an 'evaluate each run on a X "
|
|
390
486
|
"basis' instruction your goal never asked for.\n"
|
|
391
487
|
)
|
|
@@ -409,11 +505,10 @@ def generate_simulated_alert_response(
|
|
|
409
505
|
" when every one of those fields matches your goal.\n"
|
|
410
506
|
" A field your goal reports as 'not specified' is one you have NO expectation about: "
|
|
411
507
|
" accept whatever the agent chose for it and never ask for it to be removed.\n"
|
|
412
|
-
|
|
413
|
-
" Do not wait for the agent to ask — state it alongside the metric and condition answers.\n"
|
|
508
|
+
+ recipients_rule
|
|
414
509
|
+ filters_rule
|
|
415
510
|
+ f"6. Proactively state how often you want to be alerted in your first reply: {trigger_request}. "
|
|
416
|
-
" Repeat it if the agent proposes a different cadence.\n" +
|
|
511
|
+
" Repeat it if the agent proposes a different cadence.\n" + period_rule + "Reply concisely and directly."
|
|
417
512
|
)
|
|
418
513
|
|
|
419
514
|
messages: list = [{"role": "system", "content": system_prompt}]
|
|
@@ -590,6 +685,7 @@ def _normalize_expected_output(expected: dict) -> CatalogMetricAlert:
|
|
|
590
685
|
recipients = [r.strip() for r in raw_recip.replace(";", ",").split(",") if r.strip()]
|
|
591
686
|
else:
|
|
592
687
|
recipients = list(raw_recip)
|
|
688
|
+
recipients = [_CURRENT_USER_RECIPIENT if str(r).strip().lower() in _CURRENT_USER_MARKERS else r for r in recipients]
|
|
593
689
|
|
|
594
690
|
filters = _normalize_expected_filters(expected)
|
|
595
691
|
attributes = _normalize_expected_attributes(expected)
|
|
@@ -665,6 +761,12 @@ def run_agentic_alert_skill(
|
|
|
665
761
|
) -> AgenticAlertSummary:
|
|
666
762
|
"""Run the alert-skill agentic evaluation K times and return a summary."""
|
|
667
763
|
expected = _normalize_expected_output(expected_output)
|
|
764
|
+
current_user_id: str | None = None
|
|
765
|
+
if _CURRENT_USER_RECIPIENT in expected.recipients:
|
|
766
|
+
current_user_id = _resolve_current_user_id(host, token)
|
|
767
|
+
if current_user_id is None:
|
|
768
|
+
# A harness fault: report it as an item error, not as the agent's recipient mistake.
|
|
769
|
+
raise RuntimeError("Cannot score a current-user alert: the token owner's user id could not be resolved")
|
|
668
770
|
run_results: list[AlertRunResult] = []
|
|
669
771
|
client = ChatClient(
|
|
670
772
|
host=host,
|
|
@@ -761,7 +863,11 @@ def run_agentic_alert_skill(
|
|
|
761
863
|
# exit reason to classify the run as an error rather than an agent failure.
|
|
762
864
|
try:
|
|
763
865
|
follow_up = generate_simulated_alert_response(
|
|
764
|
-
response_text,
|
|
866
|
+
response_text,
|
|
867
|
+
expected,
|
|
868
|
+
conversation_history,
|
|
869
|
+
question=question,
|
|
870
|
+
current_user_id=current_user_id,
|
|
765
871
|
)
|
|
766
872
|
except Exception as exc: # noqa: BLE001 -- harness-side fault; end only this run
|
|
767
873
|
print(f"[SIM-USER] Simulated reply failed for conversation {conv_id}: {exc}")
|
|
@@ -779,7 +885,8 @@ def run_agentic_alert_skill(
|
|
|
779
885
|
trigger_correct=tool_called and _check_trigger(expected, actual_args),
|
|
780
886
|
filters_correct=tool_called and _check_filters(expected, actual_args),
|
|
781
887
|
metric_correct=tool_called and _check_metric(expected, actual_args),
|
|
782
|
-
recipients_correct=tool_called
|
|
888
|
+
recipients_correct=tool_called
|
|
889
|
+
and _check_recipients(expected, actual_args, sdk=sdk, current_user_id=current_user_id),
|
|
783
890
|
attributes_correct=tool_called and _check_attributes(expected, actual_args),
|
|
784
891
|
granularity_correct=tool_called and _check_granularity(expected, actual_args),
|
|
785
892
|
)
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/document_skill.py
RENAMED
|
@@ -1017,7 +1017,7 @@ def evaluate_agentic_document_skill(
|
|
|
1017
1017
|
elif not best.evaluation.skill_activated:
|
|
1018
1018
|
skill_note = (
|
|
1019
1019
|
f" set_skills never activated {_BUILDER_SKILL}: either the copilot routed elsewhere, or the skill"
|
|
1020
|
-
" is not registered. It registers only with
|
|
1020
|
+
" is not registered. It registers only with enableGenAiReportBuilderSkill and the org's"
|
|
1021
1021
|
" enableBusinessBriefingReportsApp both on."
|
|
1022
1022
|
)
|
|
1023
1023
|
exc = DocumentSkillAssertionError(
|
|
@@ -15,6 +15,7 @@ from gooddata_eval.core.agentic.alert_skill import (
|
|
|
15
15
|
_check_trigger,
|
|
16
16
|
_deep_subset,
|
|
17
17
|
_normalize_expected_output,
|
|
18
|
+
_resolve_current_user_id,
|
|
18
19
|
_to_number,
|
|
19
20
|
evaluate_agentic_alert_skill,
|
|
20
21
|
generate_simulated_alert_response,
|
|
@@ -289,6 +290,202 @@ def test_check_recipients_resolution_failure_fails_gracefully():
|
|
|
289
290
|
assert _check_recipients(expected, {"internal_recipients": ["user.abc123"]}, sdk=mock_sdk) is False
|
|
290
291
|
|
|
291
292
|
|
|
293
|
+
def test_check_recipients_logs_when_no_user_has_the_expected_email(capsys: pytest.CaptureFixture[str]) -> None:
|
|
294
|
+
expected = _normalize_expected_output({"Recipients": ["user@example.com"]})
|
|
295
|
+
mock_sdk = MagicMock()
|
|
296
|
+
mock_sdk._client.entities_api.get_all_entities_users.return_value.data = []
|
|
297
|
+
assert _check_recipients(expected, {"internal_recipients": ["user.abc123"]}, sdk=mock_sdk) is False
|
|
298
|
+
out = capsys.readouterr().out
|
|
299
|
+
assert "No user has any of the 1 recipient email(s)" in out
|
|
300
|
+
assert "user@example.com" not in out
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_check_recipients_logs_a_failed_email_lookup(capsys: pytest.CaptureFixture[str]) -> None:
|
|
304
|
+
expected = _normalize_expected_output({"Recipients": ["user@example.com"]})
|
|
305
|
+
mock_sdk = MagicMock()
|
|
306
|
+
mock_sdk._client.entities_api.get_all_entities_users.side_effect = RuntimeError("boom")
|
|
307
|
+
assert _check_recipients(expected, {"internal_recipients": ["user.abc123"]}, sdk=mock_sdk) is False
|
|
308
|
+
assert "failed: boom" in capsys.readouterr().out
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# --- Current user as the recipient ------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@pytest.mark.parametrize("spelling", ["me", "Me", " current user ", "current_user", "myself"])
|
|
315
|
+
def test_current_user_recipient_spellings_normalize_to_me(spelling: str) -> None:
|
|
316
|
+
assert _normalize_expected_output({"Recipient(s)": spelling}).recipients == ["me"]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_check_recipients_accepts_the_current_user_by_id() -> None:
|
|
320
|
+
expected = _normalize_expected_output({"Recipient(s)": "me"})
|
|
321
|
+
mock_sdk = MagicMock()
|
|
322
|
+
assert _check_recipients(expected, {"internal_recipients": ["admin"]}, sdk=mock_sdk, current_user_id="admin")
|
|
323
|
+
assert _check_recipients(expected, {"internal_recipients": "admin"}, sdk=mock_sdk, current_user_id="admin")
|
|
324
|
+
mock_sdk._client.entities_api.get_all_entities_users.assert_not_called()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@pytest.mark.parametrize(
|
|
328
|
+
"actual_args",
|
|
329
|
+
[
|
|
330
|
+
{"internal_recipients": ["someone.else"]},
|
|
331
|
+
{"external_recipients": ["admin@gooddata.com"]},
|
|
332
|
+
{"internal_recipients": ["admin", "someone.else"]},
|
|
333
|
+
{"internal_recipients": ["admin"], "external_recipients": ["admin@gooddata.com"]},
|
|
334
|
+
{},
|
|
335
|
+
],
|
|
336
|
+
)
|
|
337
|
+
def test_check_recipients_rejects_anything_but_exactly_the_current_user(actual_args: dict) -> None:
|
|
338
|
+
expected = _normalize_expected_output({"Recipient(s)": "me"})
|
|
339
|
+
assert _check_recipients(expected, actual_args, sdk=MagicMock(), current_user_id="admin") is False
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_check_recipients_fails_when_the_current_user_is_unknown() -> None:
|
|
343
|
+
expected = _normalize_expected_output({"Recipient(s)": "me"})
|
|
344
|
+
assert _check_recipients(expected, {"internal_recipients": ["admin"]}, current_user_id=None) is False
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_check_recipients_accepts_the_current_user_alongside_an_email() -> None:
|
|
348
|
+
expected = _normalize_expected_output({"Recipient(s)": "me, bob@example.com"})
|
|
349
|
+
args = {"internal_recipients": ["admin"], "external_recipients": ["bob@example.com"]}
|
|
350
|
+
assert _check_recipients(expected, args, current_user_id="admin") is True
|
|
351
|
+
assert _check_recipients(expected, {"internal_recipients": ["admin"]}, current_user_id="admin") is False
|
|
352
|
+
extra = {"internal_recipients": ["admin", "carol.id"], "external_recipients": ["bob@example.com"]}
|
|
353
|
+
assert _check_recipients(expected, extra, current_user_id="admin") is False
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_check_recipients_accepts_an_email_delivered_as_a_user_id_next_to_the_current_user() -> None:
|
|
357
|
+
expected = _normalize_expected_output({"Recipient(s)": "me, bob@example.com"})
|
|
358
|
+
mock_sdk = MagicMock()
|
|
359
|
+
mock_sdk._client.entities_api.get_all_entities_users.return_value.data = [MagicMock(id="bob.id")]
|
|
360
|
+
args = {"internal_recipients": ["admin", "bob.id"]}
|
|
361
|
+
assert _check_recipients(expected, args, sdk=mock_sdk, current_user_id="admin") is True
|
|
362
|
+
mock_sdk._client.entities_api.get_all_entities_users.assert_called_once_with(filter="email=in=('bob@example.com')")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_check_recipients_rejects_a_user_id_that_matches_no_expected_email() -> None:
|
|
366
|
+
expected = _normalize_expected_output({"Recipient(s)": "me, bob@example.com"})
|
|
367
|
+
mock_sdk = MagicMock()
|
|
368
|
+
mock_sdk._client.entities_api.get_all_entities_users.return_value.data = [MagicMock(id="bob.id")]
|
|
369
|
+
args = {"internal_recipients": ["admin", "bob.id", "carol.id"]}
|
|
370
|
+
assert _check_recipients(expected, args, sdk=mock_sdk, current_user_id="admin") is False
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_check_recipients_rejects_an_expected_email_that_resolves_to_no_user() -> None:
|
|
374
|
+
expected = _normalize_expected_output({"Recipient(s)": "me, bob@example.com, carol@example.com"})
|
|
375
|
+
mock_sdk = MagicMock()
|
|
376
|
+
bob, nobody = MagicMock(data=[MagicMock(id="bob.id")]), MagicMock(data=[])
|
|
377
|
+
mock_sdk._client.entities_api.get_all_entities_users.side_effect = [bob, nobody]
|
|
378
|
+
args = {"internal_recipients": ["admin", "bob.id"]}
|
|
379
|
+
assert _check_recipients(expected, args, sdk=mock_sdk, current_user_id="admin") is False
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def test_resolve_current_user_id_reads_the_profile() -> None:
|
|
383
|
+
with patch("gooddata_eval.core.agentic.alert_skill.httpx.get") as mock_get:
|
|
384
|
+
mock_get.return_value.json.return_value = {"userId": "admin", "organizationId": "org"}
|
|
385
|
+
assert _resolve_current_user_id("http://host/", "tok") == "admin"
|
|
386
|
+
mock_get.assert_called_once_with(
|
|
387
|
+
"http://host/api/v1/profile", headers={"Authorization": "Bearer tok"}, timeout=30.0
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_resolve_current_user_id_logs_and_returns_none_on_failure(capsys: pytest.CaptureFixture[str]) -> None:
|
|
392
|
+
with patch("gooddata_eval.core.agentic.alert_skill.httpx.get", side_effect=RuntimeError("down")):
|
|
393
|
+
assert _resolve_current_user_id("http://host", "tok") is None
|
|
394
|
+
assert "Resolving the current user failed: down" in capsys.readouterr().out
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_sim_user_for_a_current_user_alert_gives_no_email() -> None:
|
|
398
|
+
prompt = _sim_user_prompt({"Operator": "LESS_THAN", "Recipient(s)": "me"}, current_user_id="admin")
|
|
399
|
+
assert "recipients=me (yourself, the current user, user id admin)" in prompt
|
|
400
|
+
assert "Never give an email address" in prompt
|
|
401
|
+
assert "'You'" in prompt
|
|
402
|
+
assert "Proactively include your email recipient" not in prompt
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def test_sim_user_still_volunteers_an_email_recipient() -> None:
|
|
406
|
+
prompt = _sim_user_prompt({"Operator": "LESS_THAN", "Recipient(s)": "bob@example.com"})
|
|
407
|
+
assert "recipients=bob@example.com" in prompt
|
|
408
|
+
assert "Proactively include your email recipient" in prompt
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def test_run_agentic_alert_skill_scores_a_current_user_alert() -> None:
|
|
412
|
+
asked_turn = ChatResult.model_validate({"text_response": "Which dashboard?", "tool_call_events": []})
|
|
413
|
+
created_turn = ChatResult.model_validate(
|
|
414
|
+
{
|
|
415
|
+
"text_response": "Alert created.",
|
|
416
|
+
"tool_call_events": [
|
|
417
|
+
{
|
|
418
|
+
"functionName": "create_metric_alert",
|
|
419
|
+
"functionArguments": '{"operator": "LESS_THAN", "threshold": 30000, "internal_recipients": ["admin"]}',
|
|
420
|
+
"result": '{"id": "alert-1"}',
|
|
421
|
+
}
|
|
422
|
+
],
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
mock_client = MagicMock()
|
|
426
|
+
mock_client.send_message.side_effect = [asked_turn, created_turn]
|
|
427
|
+
|
|
428
|
+
with (
|
|
429
|
+
_patched(mock_client, simulated_reply="Any dashboard is fine.", delete_alert=True) as mock_sim,
|
|
430
|
+
patch("gooddata_eval.core.agentic.alert_skill._resolve_current_user_id", return_value="admin") as mock_me,
|
|
431
|
+
):
|
|
432
|
+
summary = run_agentic_alert_skill(
|
|
433
|
+
host="http://host",
|
|
434
|
+
token="tok",
|
|
435
|
+
workspace_id="ws1",
|
|
436
|
+
question="Alert me when profit drops below 30000.",
|
|
437
|
+
expected_output={"Operator": "LESS_THAN", "Threshold": "30000", "Recipient(s)": "me"},
|
|
438
|
+
k=1,
|
|
439
|
+
max_iterations=6,
|
|
440
|
+
initial_conversation_id="conv-1",
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
mock_me.assert_called_once_with("http://host", "tok")
|
|
444
|
+
assert mock_sim.call_args.kwargs["current_user_id"] == "admin"
|
|
445
|
+
assert summary.best.eval.recipients_correct is True
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_run_agentic_alert_skill_skips_the_profile_lookup_for_email_recipients() -> None:
|
|
449
|
+
mock_client = MagicMock()
|
|
450
|
+
mock_client.send_message.return_value = _no_alert_chat_result()
|
|
451
|
+
with (
|
|
452
|
+
_patched(mock_client, simulated_reply="ok"),
|
|
453
|
+
patch("gooddata_eval.core.agentic.alert_skill._resolve_current_user_id") as mock_me,
|
|
454
|
+
):
|
|
455
|
+
run_agentic_alert_skill(
|
|
456
|
+
host="http://host",
|
|
457
|
+
token="tok",
|
|
458
|
+
workspace_id="ws1",
|
|
459
|
+
question="Alert me",
|
|
460
|
+
expected_output={"Operator": "LESS_THAN", "Recipient(s)": "admin@gooddata.com"},
|
|
461
|
+
k=1,
|
|
462
|
+
max_iterations=1,
|
|
463
|
+
initial_conversation_id="conv-1",
|
|
464
|
+
)
|
|
465
|
+
mock_me.assert_not_called()
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def test_run_agentic_alert_skill_errors_when_the_current_user_cannot_be_resolved() -> None:
|
|
469
|
+
mock_client = MagicMock()
|
|
470
|
+
with (
|
|
471
|
+
_patched(mock_client, simulated_reply="ok") as mock_sim,
|
|
472
|
+
patch("gooddata_eval.core.agentic.alert_skill._resolve_current_user_id", return_value=None),
|
|
473
|
+
pytest.raises(RuntimeError, match="user id could not be resolved"),
|
|
474
|
+
):
|
|
475
|
+
run_agentic_alert_skill(
|
|
476
|
+
host="http://host",
|
|
477
|
+
token="tok",
|
|
478
|
+
workspace_id="ws1",
|
|
479
|
+
question="Alert me",
|
|
480
|
+
expected_output={"Operator": "LESS_THAN", "Recipient(s)": "me"},
|
|
481
|
+
k=1,
|
|
482
|
+
max_iterations=1,
|
|
483
|
+
initial_conversation_id="conv-1",
|
|
484
|
+
)
|
|
485
|
+
mock_client.send_message.assert_not_called()
|
|
486
|
+
mock_sim.assert_not_called()
|
|
487
|
+
|
|
488
|
+
|
|
292
489
|
def test_alert_evaluation_strict_pass():
|
|
293
490
|
ev = AlertEvaluation(
|
|
294
491
|
alert_created=True,
|
|
@@ -382,7 +579,7 @@ def test_run_agentic_alert_skill_creates_fresh_conversations_for_remaining_runs(
|
|
|
382
579
|
# summary that plainly read "Trigger: once per month".
|
|
383
580
|
|
|
384
581
|
|
|
385
|
-
def _sim_user_prompt(expected_output: dict, question: str = "") -> str:
|
|
582
|
+
def _sim_user_prompt(expected_output: dict, question: str = "", current_user_id: str | None = None) -> str:
|
|
386
583
|
"""Run the sim-user against a stub OpenAI client and return the system prompt it built."""
|
|
387
584
|
fake_openai = MagicMock()
|
|
388
585
|
fake_openai.return_value.chat.completions.create.return_value = MagicMock(
|
|
@@ -397,6 +594,7 @@ def _sim_user_prompt(expected_output: dict, question: str = "") -> str:
|
|
|
397
594
|
_normalize_expected_output(expected_output),
|
|
398
595
|
[],
|
|
399
596
|
question=question,
|
|
597
|
+
current_user_id=current_user_id,
|
|
400
598
|
)
|
|
401
599
|
call = fake_openai.return_value.chat.completions.create.call_args
|
|
402
600
|
return call.kwargs["messages"][0]["content"]
|
|
@@ -942,6 +1140,16 @@ def test_sim_user_still_refuses_an_unrequested_granularity_for_a_normal_alert():
|
|
|
942
1140
|
assert "no date filter at all" in prompt
|
|
943
1141
|
|
|
944
1142
|
|
|
1143
|
+
@pytest.mark.parametrize("operator", ["INCREASES_BY", "DECREASES_BY", "CHANGES_BY"])
|
|
1144
|
+
def test_sim_user_delegates_the_comparison_period_of_a_relative_alert(operator: str) -> None:
|
|
1145
|
+
prompt = _sim_user_prompt({"Operator": operator, "Time window/Filters": "None (All time)"})
|
|
1146
|
+
rule_7 = prompt.split("7.", 1)[1]
|
|
1147
|
+
assert "NOT a date filter" in rule_7
|
|
1148
|
+
assert "never applies to the comparison" in rule_7
|
|
1149
|
+
assert "Pick whichever date and period fit best" in rule_7
|
|
1150
|
+
assert "Do not invent an evaluation period, a granularity" not in prompt
|
|
1151
|
+
|
|
1152
|
+
|
|
945
1153
|
def test_sim_user_falls_back_to_day_when_an_anomaly_item_states_no_granularity():
|
|
946
1154
|
prompt = _sim_user_prompt({"Operator": "ANOMALY"})
|
|
947
1155
|
assert "use DAY granularity" in prompt
|
|
@@ -523,7 +523,7 @@ def test_a_failing_item_on_the_report_names_points_at_the_rename_not_the_flags(
|
|
|
523
523
|
evaluate_agentic_document_skill("https://h", "tok", "ws", "Create a document", {}, max_iterations=1)
|
|
524
524
|
|
|
525
525
|
assert "gen-ai still sends the Report names" in str(raised.value)
|
|
526
|
-
assert "
|
|
526
|
+
assert "enableGenAiReportBuilderSkill" not in str(raised.value)
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
def test_a_user_context_with_view_report_fails_before_any_request(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -669,7 +669,7 @@ def test_a_failing_item_without_document_builder_points_at_the_flags(monkeypatch
|
|
|
669
669
|
_install_client(monkeypatch, _ScriptedChatClient([turn]))
|
|
670
670
|
with pytest.raises(
|
|
671
671
|
DocumentSkillAssertionError,
|
|
672
|
-
match="
|
|
672
|
+
match="enableGenAiReportBuilderSkill and the org.s enableBusinessBriefingReportsApp",
|
|
673
673
|
):
|
|
674
674
|
evaluate_agentic_document_skill("https://h", "tok", "ws", "Create a document", {}, max_iterations=1)
|
|
675
675
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/scripts/verify_guardrail_refusal_criteria.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
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_catalog.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_failed_runs.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_langfuse.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/_trace_linker.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/conversation.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/dashboard_skill.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/guardrail.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/kda_skill.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/metric_skill.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/report_skill.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/search_tool.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/visualization.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/agentic/what_if.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/chat/sse_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/dataset/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/dataset/from_insights.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/dataset/langfuse_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_deep_subset.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_llm_judge.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_maql.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/_text_utils.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/alert_skill.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/base.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/guardrail.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/metric_skill.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/search_tool.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/evaluators/summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/client.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/experiment.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/item_scope.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/langfuse/observations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/console.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/html_report.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/reporting/json_report.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/summary/__init__.py
RENAMED
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/src/gooddata_eval/core/summary/http_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_eval-1.77.0 → gooddata_eval-1.77.1.dev2}/tests/fixtures/sse_visualization_stream.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|