gooddata-eval 1.78.1.dev2__py3-none-any.whl → 1.78.1.dev4__py3-none-any.whl
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/core/agentic/_catalog.py +9 -0
- gooddata_eval/core/agentic/alert_skill.py +230 -81
- gooddata_eval/core/agentic/document_skill.py +25 -5
- gooddata_eval/core/chat/sse_client.py +21 -2
- gooddata_eval/core/config.py +19 -0
- {gooddata_eval-1.78.1.dev2.dist-info → gooddata_eval-1.78.1.dev4.dist-info}/METADATA +2 -2
- {gooddata_eval-1.78.1.dev2.dist-info → gooddata_eval-1.78.1.dev4.dist-info}/RECORD +10 -10
- {gooddata_eval-1.78.1.dev2.dist-info → gooddata_eval-1.78.1.dev4.dist-info}/WHEEL +0 -0
- {gooddata_eval-1.78.1.dev2.dist-info → gooddata_eval-1.78.1.dev4.dist-info}/entry_points.txt +0 -0
- {gooddata_eval-1.78.1.dev2.dist-info → gooddata_eval-1.78.1.dev4.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -67,6 +67,12 @@ class CatalogMetricAlert:
|
|
|
67
67
|
"""Expected group-by attributes; ``None`` means the fixture states no expectation."""
|
|
68
68
|
granularity: AnomalyDetectionGranularity | None = None
|
|
69
69
|
"""Detection interval for an ANOMALY alert (DAY/WEEK/MONTH/...). Not a date filter."""
|
|
70
|
+
comparison: dict[str, str | int | float | None] | None = None
|
|
71
|
+
"""Expected period comparison as ``create_metric_alert`` argument names; ``None`` means unasserted."""
|
|
72
|
+
title: str | None = None
|
|
73
|
+
"""Alert name the user asked for; ``None`` means unasserted."""
|
|
74
|
+
dashboard_id: str | None = None
|
|
75
|
+
"""Dashboard the alert must be bound to; ``None`` means unasserted."""
|
|
70
76
|
|
|
71
77
|
@classmethod
|
|
72
78
|
def from_dict(cls, d: dict) -> CatalogMetricAlert:
|
|
@@ -87,4 +93,7 @@ class CatalogMetricAlert:
|
|
|
87
93
|
filters=d.get("filters"),
|
|
88
94
|
attributes=d.get("attributes"),
|
|
89
95
|
granularity=AnomalyDetectionGranularity.parse(d.get("granularity")),
|
|
96
|
+
comparison=d.get("comparison"),
|
|
97
|
+
title=d.get("title"),
|
|
98
|
+
dashboard_id=d.get("dashboard_id"),
|
|
90
99
|
)
|
|
@@ -58,6 +58,37 @@ _TRIGGER_DISPLAY_TO_API = {"Every time": "ALWAYS", "One time": "ONCE"}
|
|
|
58
58
|
_ALWAYS_TRIGGER_VALUES = {"Every time", "ALWAYS", "not specified"}
|
|
59
59
|
_RELATIVE_OPERATORS = ("INCREASES_BY", "DECREASES_BY", "CHANGES_BY")
|
|
60
60
|
|
|
61
|
+
# Fixture spelling of each period-comparison property -> the create_metric_alert argument.
|
|
62
|
+
_COMPARISON_ARGS = {
|
|
63
|
+
"kind": "comparison_kind",
|
|
64
|
+
"periods_ago": "comparison_periods_ago",
|
|
65
|
+
"date_dataset_id": "comparison_date_dataset_id",
|
|
66
|
+
"date_attribute_id": "comparison_date_attribute_id",
|
|
67
|
+
"arithmetic_operator": "arithmetic_operator",
|
|
68
|
+
}
|
|
69
|
+
# Compared verbatim: object ids are case-sensitive, unlike the enum-valued properties.
|
|
70
|
+
_COMPARISON_ID_ARGS = ("comparison_date_dataset_id", "comparison_date_attribute_id")
|
|
71
|
+
# What create_metric_alert applies when the argument is left out, per its own contract. A
|
|
72
|
+
# metric-to-metric comparison has no periods, so periods_ago defaults only for a period kind.
|
|
73
|
+
_COMPARISON_ARG_DEFAULTS: dict[str, object] = {"comparison_periods_ago": 1, "arithmetic_operator": "DIFFERENCE"}
|
|
74
|
+
_PERIOD_COMPARISON_KINDS = ("PREVIOUS_PERIOD", "SAME_PERIOD_PRIOR_YEAR")
|
|
75
|
+
_NO_COMPARISON_MARKERS = ("none", "no comparison")
|
|
76
|
+
|
|
77
|
+
# AlertEvaluation checks, in report order. Every run is scored on the first group; the second only
|
|
78
|
+
# when the item states the matching expectation.
|
|
79
|
+
_ALERT_CHECKS = (
|
|
80
|
+
"alert_created",
|
|
81
|
+
"operator_correct",
|
|
82
|
+
"threshold_correct",
|
|
83
|
+
"trigger_correct",
|
|
84
|
+
"filters_correct",
|
|
85
|
+
"metric_correct",
|
|
86
|
+
"recipients_correct",
|
|
87
|
+
"attributes_correct",
|
|
88
|
+
"granularity_correct",
|
|
89
|
+
)
|
|
90
|
+
_CONDITIONAL_ALERT_CHECKS = ("comparison_correct", "title_correct", "dashboard_correct")
|
|
91
|
+
|
|
61
92
|
# Expected-recipient spellings that mean the owner of the API token.
|
|
62
93
|
_CURRENT_USER_RECIPIENT = "me"
|
|
63
94
|
_CURRENT_USER_MARKERS = {"me", "myself", "current user", "current_user"}
|
|
@@ -94,6 +125,29 @@ def _parse_recipients(recipients_str: str | None) -> list[str] | None:
|
|
|
94
125
|
return [r.strip() for r in recipients_str.replace(";", ",").split(",") if r.strip()]
|
|
95
126
|
|
|
96
127
|
|
|
128
|
+
def _recipient_list(value: object, *, split_bare: bool) -> list[str]:
|
|
129
|
+
"""A recipient argument as a list, decoded the way gen-ai does before calling the server.
|
|
130
|
+
|
|
131
|
+
The tool schema declares both recipient fields ``anyOf: [array of string, string, null]``,
|
|
132
|
+
and some models send the list JSON-encoded as a string (``'["admin"]'``). Empty or
|
|
133
|
+
``"null"`` means none; any other string that is not a JSON list of strings is a bare
|
|
134
|
+
value -- one user id, or with ``split_bare`` a comma/semicolon-separated email list
|
|
135
|
+
(gen-ai forwards a bare string as one entry; the split is this checker's own leniency).
|
|
136
|
+
A non-string list entry is kept as its ``str()`` so it compares unequal instead of raising.
|
|
137
|
+
"""
|
|
138
|
+
if isinstance(value, list):
|
|
139
|
+
return [str(item) for item in value]
|
|
140
|
+
if not isinstance(value, str) or value.strip() in ("", "null"):
|
|
141
|
+
return []
|
|
142
|
+
try:
|
|
143
|
+
parsed = json.loads(value)
|
|
144
|
+
except ValueError:
|
|
145
|
+
parsed = None
|
|
146
|
+
if isinstance(parsed, list) and all(isinstance(item, str) for item in parsed):
|
|
147
|
+
return parsed
|
|
148
|
+
return (_parse_recipients(value) or []) if split_bare else [value]
|
|
149
|
+
|
|
150
|
+
|
|
97
151
|
def _deep_subset(expected: object, actual: object) -> bool:
|
|
98
152
|
"""Return True if expected is a recursive subset of actual."""
|
|
99
153
|
if isinstance(expected, dict) and isinstance(actual, dict):
|
|
@@ -234,6 +288,66 @@ def _check_granularity(expected: CatalogMetricAlert, actual_args: dict) -> bool:
|
|
|
234
288
|
return str(actual).strip().upper() == expected.granularity.value
|
|
235
289
|
|
|
236
290
|
|
|
291
|
+
def _canonical_comparison_value(arg: str, value: object) -> str | int | float | None:
|
|
292
|
+
"""Fold one comparison value so fixture and tool-argument spellings compare equal."""
|
|
293
|
+
if value is None or str(value).strip() == "":
|
|
294
|
+
return None
|
|
295
|
+
if arg == "comparison_periods_ago":
|
|
296
|
+
return _to_number(value)
|
|
297
|
+
if arg in _COMPARISON_ID_ARGS:
|
|
298
|
+
return str(value).strip()
|
|
299
|
+
return str(value).strip().upper()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _check_comparison(expected: CatalogMetricAlert, actual_args: dict) -> bool:
|
|
303
|
+
"""Compare only the comparison properties the fixture states.
|
|
304
|
+
|
|
305
|
+
``None`` means unasserted. A stated ``comparison_kind`` of ``None`` asserts the alert has
|
|
306
|
+
no comparison at all. ``comparison_kind`` is read as ``create_metric_alert`` resolves it:
|
|
307
|
+
left out next to a ``compare_metric_id`` it is METRIC. Any other argument the agent left out
|
|
308
|
+
on an alert that does compare counts as the value ``create_metric_alert`` applies for it, so
|
|
309
|
+
omitting ``comparison_periods_ago`` matches 1 on a period comparison; a METRIC comparison has
|
|
310
|
+
no periods, and with no comparison there is nothing to default.
|
|
311
|
+
The comparison period itself (DAY, MONTH, ...) is not checked here: it is the granularity
|
|
312
|
+
of the comparison date filter, which ``filters`` asserts.
|
|
313
|
+
"""
|
|
314
|
+
if expected.comparison is None:
|
|
315
|
+
return True
|
|
316
|
+
kind = actual_args.get("comparison_kind") or ("METRIC" if actual_args.get("compare_metric_id") else None)
|
|
317
|
+
for arg, exp_value in expected.comparison.items():
|
|
318
|
+
act_raw = kind if arg == "comparison_kind" else actual_args.get(arg)
|
|
319
|
+
defaults_apply = arg != "comparison_periods_ago" or str(kind).upper() in _PERIOD_COMPARISON_KINDS
|
|
320
|
+
if act_raw is None and kind is not None and defaults_apply:
|
|
321
|
+
act_raw = _COMPARISON_ARG_DEFAULTS.get(arg)
|
|
322
|
+
# Only the fixture side of an object id is trimmed: a padded id is a different id.
|
|
323
|
+
actual = act_raw if arg in _COMPARISON_ID_ARGS else _canonical_comparison_value(arg, act_raw)
|
|
324
|
+
if actual != exp_value:
|
|
325
|
+
return False
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _check_title(expected: CatalogMetricAlert, actual_args: dict) -> bool:
|
|
330
|
+
"""Exact match on the alert name when the fixture states one.
|
|
331
|
+
|
|
332
|
+
Only the fixture side is trimmed: a name saved with padding is not the name the user asked for.
|
|
333
|
+
|
|
334
|
+
An omitted ``title`` fails: ``create_metric_alert`` then saves an auto-generated name, even
|
|
335
|
+
when ``automation_id`` carries a slug of the requested one.
|
|
336
|
+
"""
|
|
337
|
+
if expected.title is None:
|
|
338
|
+
return True
|
|
339
|
+
actual = actual_args.get("title")
|
|
340
|
+
return isinstance(actual, str) and actual == expected.title
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _check_dashboard(expected: CatalogMetricAlert, actual_args: dict) -> bool:
|
|
344
|
+
"""Compare the bound dashboard id when the fixture states one; the actual id verbatim."""
|
|
345
|
+
if expected.dashboard_id is None:
|
|
346
|
+
return True
|
|
347
|
+
actual = actual_args.get("dashboard_id")
|
|
348
|
+
return isinstance(actual, str) and actual == expected.dashboard_id
|
|
349
|
+
|
|
350
|
+
|
|
237
351
|
def _check_metric(expected: CatalogMetricAlert, actual_args: dict) -> bool:
|
|
238
352
|
if not expected.metric_id:
|
|
239
353
|
return True
|
|
@@ -305,35 +419,14 @@ def _check_recipients(
|
|
|
305
419
|
) -> bool:
|
|
306
420
|
if not expected.recipients:
|
|
307
421
|
return True
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
# external_recipients is JSON-encoded (e.g. '["email@example.com"]')
|
|
311
|
-
try:
|
|
312
|
-
parsed = json.loads(act_recip_raw)
|
|
313
|
-
act_recip = parsed if isinstance(parsed, list) else _parse_recipients(act_recip_raw)
|
|
314
|
-
except (json.JSONDecodeError, ValueError):
|
|
315
|
-
act_recip = _parse_recipients(act_recip_raw)
|
|
316
|
-
elif isinstance(act_recip_raw, list):
|
|
317
|
-
act_recip = act_recip_raw
|
|
318
|
-
else:
|
|
319
|
-
act_recip = []
|
|
320
|
-
act_internal_raw = actual_args.get("internal_recipients")
|
|
321
|
-
# internal_recipients is declared `anyOf: [array of string, string, null]` in the
|
|
322
|
-
# create_metric_alert tool schema -- a single id as a bare string is schema-legal,
|
|
323
|
-
# not a malformed call, so it needs the same string/list normalization already
|
|
324
|
-
# applied to recipients/external_recipients above.
|
|
325
|
-
if isinstance(act_internal_raw, str):
|
|
326
|
-
act_internal = [act_internal_raw]
|
|
327
|
-
elif isinstance(act_internal_raw, list):
|
|
328
|
-
act_internal = act_internal_raw
|
|
329
|
-
else:
|
|
330
|
-
act_internal = []
|
|
422
|
+
act_recip = _recipient_list(actual_args.get("recipients", actual_args.get("external_recipients")), split_bare=True)
|
|
423
|
+
act_internal = _recipient_list(actual_args.get("internal_recipients"), split_bare=False)
|
|
331
424
|
wants_current_user, expected_emails = _split_current_user(expected.recipients)
|
|
332
425
|
if wants_current_user:
|
|
333
426
|
if current_user_id is None or current_user_id not in act_internal:
|
|
334
427
|
return False
|
|
335
428
|
other_internal = set(act_internal) - {current_user_id}
|
|
336
|
-
external = set(act_recip
|
|
429
|
+
external = set(act_recip)
|
|
337
430
|
if external - set(expected_emails):
|
|
338
431
|
return False
|
|
339
432
|
# An email not sent externally must arrive as its user's id (workspace-restricted channel).
|
|
@@ -345,7 +438,7 @@ def _check_recipients(
|
|
|
345
438
|
# One lookup per email: a bulk lookup returns only the users that exist, hiding a missing one.
|
|
346
439
|
ids_per_email = [_resolve_internal_recipient_ids(sdk, [email]) for email in unmatched]
|
|
347
440
|
return all(ids_per_email) and other_internal == set().union(*ids_per_email)
|
|
348
|
-
if set(expected_emails) == set(act_recip
|
|
441
|
+
if set(expected_emails) == set(act_recip):
|
|
349
442
|
return True
|
|
350
443
|
if sdk is not None and act_internal:
|
|
351
444
|
internal_recipient_ids = _resolve_internal_recipient_ids(sdk, expected_emails)
|
|
@@ -489,6 +582,15 @@ def generate_simulated_alert_response(
|
|
|
489
582
|
|
|
490
583
|
original_request = f'Your original request to the agent was: "{question}"\n' if question else ""
|
|
491
584
|
|
|
585
|
+
# Only a stated title is enforced: an unstated one is the agent's to choose, and telling the
|
|
586
|
+
# sim-user nothing keeps it from objecting to a generated name.
|
|
587
|
+
title_rule = (
|
|
588
|
+
f'8. The alert must be named exactly "{expected.title}". If a summary or proposal shows a '
|
|
589
|
+
"different name, do NOT confirm: state that exact name and ask the agent to use it.\n"
|
|
590
|
+
if expected.title
|
|
591
|
+
else ""
|
|
592
|
+
)
|
|
593
|
+
|
|
492
594
|
system_prompt = (
|
|
493
595
|
"You are a user requesting creation of an alert for a metric from an AI agent. "
|
|
494
596
|
"Respond naturally but always steer toward the exact values you were given.\n"
|
|
@@ -509,7 +611,10 @@ def generate_simulated_alert_response(
|
|
|
509
611
|
+ recipients_rule
|
|
510
612
|
+ filters_rule
|
|
511
613
|
+ f"6. Proactively state how often you want to be alerted in your first reply: {trigger_request}. "
|
|
512
|
-
" Repeat it if the agent proposes a different cadence.\n"
|
|
614
|
+
" Repeat it if the agent proposes a different cadence.\n"
|
|
615
|
+
+ period_rule
|
|
616
|
+
+ title_rule
|
|
617
|
+
+ "Reply concisely and directly."
|
|
513
618
|
)
|
|
514
619
|
|
|
515
620
|
messages: list = [{"role": "system", "content": system_prompt}]
|
|
@@ -550,22 +655,28 @@ class AlertEvaluation:
|
|
|
550
655
|
recipients_correct: bool
|
|
551
656
|
attributes_correct: bool = True
|
|
552
657
|
granularity_correct: bool = True
|
|
658
|
+
# ``None``: the item states no such expectation, so the check is neither scored nor gated.
|
|
659
|
+
comparison_correct: bool | None = None
|
|
660
|
+
title_correct: bool | None = None
|
|
661
|
+
dashboard_correct: bool | None = None
|
|
662
|
+
|
|
663
|
+
def checks(self) -> dict[str, bool]:
|
|
664
|
+
"""Every check that ran, by name; strict_pass requires all of them.
|
|
665
|
+
|
|
666
|
+
The conditional checks are left out when unasserted, so an item that names no title does
|
|
667
|
+
not report a ``title_correct`` it was never scored on, and a refusal on it is not counted
|
|
668
|
+
as a title failure.
|
|
669
|
+
"""
|
|
670
|
+
ran: dict[str, bool] = {name: getattr(self, name) for name in _ALERT_CHECKS}
|
|
671
|
+
for name in _CONDITIONAL_ALERT_CHECKS:
|
|
672
|
+
value = getattr(self, name)
|
|
673
|
+
if value is not None:
|
|
674
|
+
ran[name] = value
|
|
675
|
+
return ran
|
|
553
676
|
|
|
554
677
|
@property
|
|
555
678
|
def strict_pass(self) -> bool:
|
|
556
|
-
return all(
|
|
557
|
-
[
|
|
558
|
-
self.alert_created,
|
|
559
|
-
self.operator_correct,
|
|
560
|
-
self.threshold_correct,
|
|
561
|
-
self.trigger_correct,
|
|
562
|
-
self.filters_correct,
|
|
563
|
-
self.metric_correct,
|
|
564
|
-
self.recipients_correct,
|
|
565
|
-
self.attributes_correct,
|
|
566
|
-
self.granularity_correct,
|
|
567
|
-
]
|
|
568
|
-
)
|
|
679
|
+
return all(self.checks().values())
|
|
569
680
|
|
|
570
681
|
|
|
571
682
|
@dataclass
|
|
@@ -666,6 +777,68 @@ def _normalize_expected_attributes(expected: dict) -> list | None:
|
|
|
666
777
|
raise ValueError(f"Attributes expectation must be a list or a display string, got {type(attributes).__name__}")
|
|
667
778
|
|
|
668
779
|
|
|
780
|
+
def _normalize_expected_comparison(expected: dict) -> dict[str, str | int | float | None] | None:
|
|
781
|
+
"""
|
|
782
|
+
* ``Comparison`` object -> its stated properties, keyed by tool-argument name
|
|
783
|
+
* "None" / "no comparison" -> ``{"comparison_kind": None}`` (stated: no comparison)
|
|
784
|
+
* absent, empty, or prose -> ``None`` (unstated; comparison not asserted)
|
|
785
|
+
|
|
786
|
+
Each property of the object is asserted only when it carries a value, so an item can pin
|
|
787
|
+
the comparison kind without also pinning the date dataset the agent may pick.
|
|
788
|
+
"""
|
|
789
|
+
comparison = _case_insensitive_get(expected, "comparison")
|
|
790
|
+
if comparison is None:
|
|
791
|
+
return None
|
|
792
|
+
if isinstance(comparison, str):
|
|
793
|
+
return {"comparison_kind": None} if any(kw in comparison.lower() for kw in _NO_COMPARISON_MARKERS) else None
|
|
794
|
+
if not isinstance(comparison, dict):
|
|
795
|
+
raise ValueError(f"Comparison expectation must be an object or a display string, got {comparison!r}")
|
|
796
|
+
stated: dict[str, str | int | float | None] = {}
|
|
797
|
+
for key, value in comparison.items():
|
|
798
|
+
name = str(key).strip().lower()
|
|
799
|
+
arg = _COMPARISON_ARGS.get(name) or (name if name in _COMPARISON_ARGS.values() else None)
|
|
800
|
+
if arg is None:
|
|
801
|
+
# Validated here so a misspelled property fails before the run spends an API call.
|
|
802
|
+
raise ValueError(f"Unrecognised comparison property {key!r}; expected one of {sorted(_COMPARISON_ARGS)}")
|
|
803
|
+
canonical = _canonical_comparison_value(arg, value)
|
|
804
|
+
if canonical is None and arg == "comparison_periods_ago" and str(value or "").strip():
|
|
805
|
+
raise ValueError(f"Comparison periods_ago must be a number, got {value!r}")
|
|
806
|
+
if canonical is not None:
|
|
807
|
+
stated[arg] = canonical
|
|
808
|
+
return stated or None
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _normalize_expected_text(expected: dict, *keys: str) -> str | None:
|
|
812
|
+
"""A stated string expectation, or ``None`` when absent or blank (unasserted)."""
|
|
813
|
+
value = _case_insensitive_get(expected, *keys)
|
|
814
|
+
if value is None:
|
|
815
|
+
return None
|
|
816
|
+
if not isinstance(value, str):
|
|
817
|
+
raise ValueError(f"{keys[0]} expectation must be a string, got {value!r}")
|
|
818
|
+
return value.strip() or None
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _normalize_expected_dashboard(expected: dict) -> str | None:
|
|
822
|
+
"""
|
|
823
|
+
* ``dashboard_id`` -> that id
|
|
824
|
+
* ``Dashboard`` "Title (id)" -> the id in the trailing parentheses, as ``Metric`` is read
|
|
825
|
+
* absent / empty -> ``None`` (unstated; binding not asserted)
|
|
826
|
+
|
|
827
|
+
A ``Dashboard`` without a trailing ``(id)`` raises: a title alone can never equal the
|
|
828
|
+
``dashboard_id`` the tool receives, so the item would fail on every run.
|
|
829
|
+
"""
|
|
830
|
+
dashboard_id = _normalize_expected_text(expected, "dashboard_id")
|
|
831
|
+
if dashboard_id is not None:
|
|
832
|
+
return dashboard_id
|
|
833
|
+
display = _normalize_expected_text(expected, "dashboard")
|
|
834
|
+
if display is None:
|
|
835
|
+
return None
|
|
836
|
+
parsed = _parse_metric_id(display)
|
|
837
|
+
if parsed is None:
|
|
838
|
+
raise ValueError(f'Dashboard expectation must be "Title (id)", got {display!r}; use dashboard_id for a bare id')
|
|
839
|
+
return parsed
|
|
840
|
+
|
|
841
|
+
|
|
669
842
|
def _normalize_expected_output(expected: dict) -> CatalogMetricAlert:
|
|
670
843
|
"""Parse expected_output dict into CatalogMetricAlert, accepting display-format or internal-format keys."""
|
|
671
844
|
operator = _case_insensitive_get(expected, "operator") or "GREATER_THAN"
|
|
@@ -696,6 +869,9 @@ def _normalize_expected_output(expected: dict) -> CatalogMetricAlert:
|
|
|
696
869
|
granularity = AnomalyDetectionGranularity.parse(
|
|
697
870
|
_case_insensitive_get(expected, "granularity", "detection granularity")
|
|
698
871
|
)
|
|
872
|
+
comparison = _normalize_expected_comparison(expected)
|
|
873
|
+
title = _normalize_expected_text(expected, "title")
|
|
874
|
+
dashboard_id = _normalize_expected_dashboard(expected)
|
|
699
875
|
|
|
700
876
|
return CatalogMetricAlert(
|
|
701
877
|
operator=operator,
|
|
@@ -708,6 +884,9 @@ def _normalize_expected_output(expected: dict) -> CatalogMetricAlert:
|
|
|
708
884
|
filters=filters,
|
|
709
885
|
attributes=attributes,
|
|
710
886
|
granularity=granularity,
|
|
887
|
+
comparison=comparison,
|
|
888
|
+
title=title,
|
|
889
|
+
dashboard_id=dashboard_id,
|
|
711
890
|
)
|
|
712
891
|
|
|
713
892
|
|
|
@@ -893,6 +1072,13 @@ def run_agentic_alert_skill(
|
|
|
893
1072
|
and _check_recipients(expected, actual_args, sdk=sdk, current_user_id=current_user_id),
|
|
894
1073
|
attributes_correct=tool_called and _check_attributes(expected, actual_args),
|
|
895
1074
|
granularity_correct=tool_called and _check_granularity(expected, actual_args),
|
|
1075
|
+
comparison_correct=None
|
|
1076
|
+
if expected.comparison is None
|
|
1077
|
+
else tool_called and _check_comparison(expected, actual_args),
|
|
1078
|
+
title_correct=None if expected.title is None else tool_called and _check_title(expected, actual_args),
|
|
1079
|
+
dashboard_correct=None
|
|
1080
|
+
if expected.dashboard_id is None
|
|
1081
|
+
else tool_called and _check_dashboard(expected, actual_args),
|
|
896
1082
|
)
|
|
897
1083
|
return AlertRunResult(
|
|
898
1084
|
conversation_id=conv_id,
|
|
@@ -934,19 +1120,7 @@ def run_agentic_alert_skill(
|
|
|
934
1120
|
pass_power_k = all(r.eval.strict_pass for r in run_results)
|
|
935
1121
|
best = max(
|
|
936
1122
|
run_results,
|
|
937
|
-
key=lambda r: sum(
|
|
938
|
-
[
|
|
939
|
-
r.eval.alert_created,
|
|
940
|
-
r.eval.operator_correct,
|
|
941
|
-
r.eval.threshold_correct,
|
|
942
|
-
r.eval.trigger_correct,
|
|
943
|
-
r.eval.filters_correct,
|
|
944
|
-
r.eval.metric_correct,
|
|
945
|
-
r.eval.recipients_correct,
|
|
946
|
-
r.eval.attributes_correct,
|
|
947
|
-
r.eval.granularity_correct,
|
|
948
|
-
]
|
|
949
|
-
),
|
|
1123
|
+
key=lambda r: sum(r.eval.checks().values()),
|
|
950
1124
|
)
|
|
951
1125
|
return AgenticAlertSummary(
|
|
952
1126
|
run_results=run_results,
|
|
@@ -966,17 +1140,8 @@ def _run_detail(run: AlertRunResult) -> dict:
|
|
|
966
1140
|
Extracted so a failing run is described by exactly the same keys as the winning run --
|
|
967
1141
|
for this kind that means the full per-field check breakdown and the arguments the run sent.
|
|
968
1142
|
"""
|
|
969
|
-
ev = run.eval
|
|
970
1143
|
return {
|
|
971
|
-
|
|
972
|
-
"operator_correct": ev.operator_correct,
|
|
973
|
-
"threshold_correct": ev.threshold_correct,
|
|
974
|
-
"trigger_correct": ev.trigger_correct,
|
|
975
|
-
"filters_correct": ev.filters_correct,
|
|
976
|
-
"metric_correct": ev.metric_correct,
|
|
977
|
-
"recipients_correct": ev.recipients_correct,
|
|
978
|
-
"attributes_correct": ev.attributes_correct,
|
|
979
|
-
"granularity_correct": ev.granularity_correct,
|
|
1144
|
+
**run.eval.checks(),
|
|
980
1145
|
"actual_alert_arguments": run.actual_alert_arguments,
|
|
981
1146
|
# Why the loop stopped. alert_created=False alone cannot tell a refusal from a run
|
|
982
1147
|
# that hit max_iterations while still on track -- see LoopExit. Per-run rather than
|
|
@@ -1052,18 +1217,7 @@ def evaluate_agentic_alert_skill(
|
|
|
1052
1217
|
|
|
1053
1218
|
for run_idx, run in enumerate(summary.run_results):
|
|
1054
1219
|
pt = ctx.trace(run.conversation_id)
|
|
1055
|
-
|
|
1056
|
-
strict_checks = {
|
|
1057
|
-
"alert_created": ev.alert_created,
|
|
1058
|
-
"operator_correct": ev.operator_correct,
|
|
1059
|
-
"threshold_correct": ev.threshold_correct,
|
|
1060
|
-
"trigger_correct": ev.trigger_correct,
|
|
1061
|
-
"filters_correct": ev.filters_correct,
|
|
1062
|
-
"metric_correct": ev.metric_correct,
|
|
1063
|
-
"recipients_correct": ev.recipients_correct,
|
|
1064
|
-
"attributes_correct": ev.attributes_correct,
|
|
1065
|
-
"granularity_correct": ev.granularity_correct,
|
|
1066
|
-
}
|
|
1220
|
+
strict_checks = run.eval.checks()
|
|
1067
1221
|
with ctx.observe(pt, run_idx, conversation_id=run.conversation_id, output=strict_checks) as tid:
|
|
1068
1222
|
for score_name, value in strict_checks.items():
|
|
1069
1223
|
ctx.score(tid, name=score_name, value=float(value), data_type="BOOLEAN")
|
|
@@ -1112,13 +1266,8 @@ def evaluate_agentic_alert_skill(
|
|
|
1112
1266
|
gate_note = gate_failure_note(gate, runs_passed, runs_effective)
|
|
1113
1267
|
exc = AlertSkillAssertionError(
|
|
1114
1268
|
f"Alert skill assertion failed. {gate_note} strict_pass={ev.strict_pass}. "
|
|
1115
|
-
f"
|
|
1116
|
-
f"
|
|
1117
|
-
f"filters_correct={ev.filters_correct}, metric_correct={ev.metric_correct}, "
|
|
1118
|
-
f"recipients_correct={ev.recipients_correct}, "
|
|
1119
|
-
f"attributes_correct={ev.attributes_correct}, "
|
|
1120
|
-
f"granularity_correct={ev.granularity_correct}. "
|
|
1121
|
-
f"Actual args: {best.actual_alert_arguments}"
|
|
1269
|
+
+ ", ".join(f"{name}={value}" for name, value in ev.checks().items())
|
|
1270
|
+
+ f". Actual args: {best.actual_alert_arguments}"
|
|
1122
1271
|
)
|
|
1123
1272
|
exc.reasoning_steps = best.reasoning_steps
|
|
1124
1273
|
exc.conversation_id = best.conversation_id
|
|
@@ -32,7 +32,7 @@ from gooddata_eval.core.agentic._trace_linker import (
|
|
|
32
32
|
from gooddata_eval.core.agentic.dashboard_skill import _extract_tool_result, _skill_activated
|
|
33
33
|
from gooddata_eval.core.chat.render import render_answer_text
|
|
34
34
|
from gooddata_eval.core.chat.sse_client import ChatClient
|
|
35
|
-
from gooddata_eval.core.config import ReasoningEffort
|
|
35
|
+
from gooddata_eval.core.config import DocumentFormat, ReasoningEffort, normalize_document_format
|
|
36
36
|
from gooddata_eval.core.evaluators._llm_judge import JudgeResponseError, LLMJudge, score_run
|
|
37
37
|
from gooddata_eval.core.langfuse.item_scope import item_scope
|
|
38
38
|
from gooddata_eval.core.models import (
|
|
@@ -801,18 +801,24 @@ def run_agentic_document_skill(
|
|
|
801
801
|
agent_id: str | None = None,
|
|
802
802
|
judge: LLMJudge | None = None,
|
|
803
803
|
user_context: dict | None = None,
|
|
804
|
+
document_format: DocumentFormat | None = None,
|
|
804
805
|
) -> AgenticDocumentSummary:
|
|
805
806
|
"""Run the document-skill agentic evaluation K times and return a summary.
|
|
806
807
|
|
|
807
808
|
The narrative judge is built only for a fixture that states a ``narrative``, so an item
|
|
808
809
|
without one needs neither the llm-judge extra nor ``OPENAI_API_KEY``.
|
|
809
810
|
|
|
811
|
+
``document_format`` is sent on every turn of every run, see ``ChatClient``; None leaves
|
|
812
|
+
the format to the server's ``enablePublisherV2`` feature flag.
|
|
813
|
+
|
|
810
814
|
Raises:
|
|
811
815
|
ValueError: the fixture is unusable — see ``_validate_expectation`` and
|
|
812
|
-
``_validate_user_context
|
|
816
|
+
``_validate_user_context``, or ``document_format`` is not a format gen-ai
|
|
817
|
+
accepts. Raised before any chat request.
|
|
813
818
|
"""
|
|
814
819
|
_validate_expectation(expected_output)
|
|
815
820
|
_validate_user_context(user_context)
|
|
821
|
+
document_format = normalize_document_format(document_format)
|
|
816
822
|
if judge is None and _has_narrative(expected_output):
|
|
817
823
|
judge = LLMJudge(_NARRATIVE_EVALUATION_STEPS)
|
|
818
824
|
run_results: list[DocumentRunResult] = []
|
|
@@ -823,6 +829,7 @@ def run_agentic_document_skill(
|
|
|
823
829
|
reasoning_effort=reasoning_effort,
|
|
824
830
|
agent_id=agent_id,
|
|
825
831
|
user_context=user_context,
|
|
832
|
+
document_format=document_format,
|
|
826
833
|
)
|
|
827
834
|
|
|
828
835
|
try:
|
|
@@ -882,20 +889,32 @@ def evaluate_agentic_document_skill(
|
|
|
882
889
|
gate: EvalGate = DEFAULT_GATE,
|
|
883
890
|
judge: LLMJudge | None = None,
|
|
884
891
|
user_context: dict | None = None,
|
|
892
|
+
document_format: DocumentFormat | None = None,
|
|
885
893
|
) -> AgenticEvalOutcome:
|
|
886
894
|
"""Run document-skill evaluation, log to Langfuse, and raise on failure.
|
|
887
895
|
|
|
888
896
|
Returns the best run's outcome on success; on failure the same values are attached to the
|
|
889
897
|
raised ``DocumentSkillAssertionError`` so callers can retrieve them either way.
|
|
890
898
|
|
|
899
|
+
``document_format`` (``aac-v1``/``aac-v2``) asks gen-ai for that document content format
|
|
900
|
+
on every turn and is recorded as ``document_format`` in the dataset-run metadata; None
|
|
901
|
+
leaves it to the server's ``enablePublisherV2`` feature flag.
|
|
902
|
+
|
|
891
903
|
Raises:
|
|
892
904
|
DocumentSkillAssertionError: the gate did not pass.
|
|
893
|
-
ValueError: the fixture is unusable
|
|
894
|
-
``_validate_user_context``. Raised before any
|
|
895
|
-
|
|
905
|
+
ValueError: the fixture is unusable, see ``_validate_expectation`` and
|
|
906
|
+
``_validate_user_context``. Raised before any chat request, so it means a
|
|
907
|
+
fixture to fix rather than a result to read. Also raised, equally before any
|
|
908
|
+
request, when the caller passes a ``document_format`` gen-ai does not accept.
|
|
896
909
|
JudgeResponseError: the fixture states a narrative and the judge returned no readable
|
|
897
910
|
verdict for any run -- an item without a result, not K failures.
|
|
898
911
|
"""
|
|
912
|
+
document_format = normalize_document_format(document_format)
|
|
913
|
+
if document_format is not None:
|
|
914
|
+
# Metadata only, not part of the run name: it tells runs apart only when their names
|
|
915
|
+
# already differ, so two runs that share a run_timestamp, dataset and model but not the
|
|
916
|
+
# format still collide on one dataset-run name.
|
|
917
|
+
run_metadata_extra = {**(run_metadata_extra or {}), "document_format": document_format}
|
|
899
918
|
identity = RunIdentity(
|
|
900
919
|
host,
|
|
901
920
|
token,
|
|
@@ -921,6 +940,7 @@ def evaluate_agentic_document_skill(
|
|
|
921
940
|
agent_id=agent_id,
|
|
922
941
|
judge=judge,
|
|
923
942
|
user_context=user_context,
|
|
943
|
+
document_format=document_format,
|
|
924
944
|
)
|
|
925
945
|
|
|
926
946
|
if langfuse is not None and dataset_item_id:
|
|
@@ -26,7 +26,12 @@ from urllib.parse import quote
|
|
|
26
26
|
import httpx
|
|
27
27
|
|
|
28
28
|
from gooddata_eval.core._retry import RETRYABLE_STATUS_CODES, float_env, int_env, retry_transient
|
|
29
|
-
from gooddata_eval.core.config import
|
|
29
|
+
from gooddata_eval.core.config import (
|
|
30
|
+
DocumentFormat,
|
|
31
|
+
ReasoningEffort,
|
|
32
|
+
normalize_document_format,
|
|
33
|
+
normalize_reasoning_effort,
|
|
34
|
+
)
|
|
30
35
|
from gooddata_eval.core.langfuse.item_scope import JoinedRun, baggage_entries, current_scope, mark_joined, trace_ids_for
|
|
31
36
|
from gooddata_eval.core.models import ChatResult, DatasetItem
|
|
32
37
|
from gooddata_eval.core.timing import note_trace_latency
|
|
@@ -460,6 +465,7 @@ class ChatClient:
|
|
|
460
465
|
reasoning_effort: ReasoningEffort | None = None,
|
|
461
466
|
agent_id: str | None = None,
|
|
462
467
|
user_context: dict[str, Any] | None = None,
|
|
468
|
+
document_format: DocumentFormat | None = None,
|
|
463
469
|
):
|
|
464
470
|
"""Create a chat client bound to one workspace.
|
|
465
471
|
|
|
@@ -476,6 +482,13 @@ class ChatClient:
|
|
|
476
482
|
back to it: a caller that changes or clears the context per message builds the
|
|
477
483
|
client without one. The server grounds answers in it only while
|
|
478
484
|
the ``enableAiContextSetup`` feature flag is on for the organization.
|
|
485
|
+
|
|
486
|
+
``document_format`` (``aac-v1``/``aac-v2``) is sent as ``options.documentFormat``
|
|
487
|
+
on every message and selects the document content format gen-ai drafts in for
|
|
488
|
+
that turn. gen-ai keeps a draft in the format of the turn that wrote it and a
|
|
489
|
+
turn in the other format cannot continue it, so follow-up and clarification
|
|
490
|
+
turns carry it too. When None the key is omitted and the server picks the format
|
|
491
|
+
from the ``enablePublisherV2`` feature flag. Any other value raises ``ValueError``.
|
|
479
492
|
"""
|
|
480
493
|
self._base = f"{host.rstrip('/')}/api/v1/ai/workspaces/{workspace_id}/chat/conversations"
|
|
481
494
|
self._auth = {"Authorization": f"Bearer {token}", **_trace_baggage()}
|
|
@@ -494,6 +507,7 @@ class ChatClient:
|
|
|
494
507
|
self._client = httpx.Client(timeout=http_timeout)
|
|
495
508
|
self._preserve_failed = preserve_failed
|
|
496
509
|
self._reasoning_effort = normalize_reasoning_effort(reasoning_effort)
|
|
510
|
+
self._document_format = normalize_document_format(document_format)
|
|
497
511
|
self._agent_id = agent_id
|
|
498
512
|
self._user_context = user_context
|
|
499
513
|
# Run index of each conversation this client has sent to, in first-send order: every
|
|
@@ -531,8 +545,13 @@ class ChatClient:
|
|
|
531
545
|
join_headers, joined = self._join_item_trace(conversation_id)
|
|
532
546
|
headers = {**self._auth, "Accept": "text/event-stream", "Content-Type": "application/json", **join_headers}
|
|
533
547
|
body: dict[str, Any] = {"item": {"role": "user", "content": {"type": "text", "text": question}}}
|
|
548
|
+
options: dict[str, str] = {}
|
|
534
549
|
if self._reasoning_effort is not None:
|
|
535
|
-
|
|
550
|
+
options["reasoningEffort"] = self._reasoning_effort
|
|
551
|
+
if self._document_format is not None:
|
|
552
|
+
options["documentFormat"] = self._document_format
|
|
553
|
+
if options:
|
|
554
|
+
body["options"] = options
|
|
536
555
|
# A per-call context overrides the client's. Only sent when there is one: gen-ai
|
|
537
556
|
# accepts an explicit null, so assigning unconditionally would quietly change every
|
|
538
557
|
# request that has no attachment.
|
gooddata_eval/core/config.py
CHANGED
|
@@ -33,6 +33,9 @@ def judge_model() -> str:
|
|
|
33
33
|
ReasoningEffort = Literal["LOW", "MEDIUM", "HIGH"]
|
|
34
34
|
"""Effort values the AI chat endpoint accepts, uppercase as the server enum requires."""
|
|
35
35
|
|
|
36
|
+
DocumentFormat = Literal["aac-v1", "aac-v2"]
|
|
37
|
+
"""Document content formats the AI chat endpoint accepts as ``options.documentFormat``."""
|
|
38
|
+
|
|
36
39
|
EvalGate = Literal["any", "power"]
|
|
37
40
|
"""`any` = pass@K, `power` = pass^K. Behaviour is in core/agentic/_gate.py; the type lives here
|
|
38
41
|
because putting it there would make config import the agentic package, whose __init__ imports
|
|
@@ -73,6 +76,22 @@ def normalize_reasoning_effort(value: str | None) -> ReasoningEffort | None:
|
|
|
73
76
|
return cast("ReasoningEffort", candidate)
|
|
74
77
|
|
|
75
78
|
|
|
79
|
+
def normalize_document_format(value: str | None) -> DocumentFormat | None:
|
|
80
|
+
"""Canonical document format, or None when unset.
|
|
81
|
+
|
|
82
|
+
gen-ai rejects any value outside `DocumentFormat`, so an unknown value raises here,
|
|
83
|
+
before a run sends its first request.
|
|
84
|
+
"""
|
|
85
|
+
if value is None:
|
|
86
|
+
return None
|
|
87
|
+
candidate = value.strip().lower()
|
|
88
|
+
if not candidate:
|
|
89
|
+
return None
|
|
90
|
+
if candidate not in get_args(DocumentFormat):
|
|
91
|
+
raise ValueError(f"Invalid document format {value!r}; expected one of {', '.join(get_args(DocumentFormat))}.")
|
|
92
|
+
return cast("DocumentFormat", candidate)
|
|
93
|
+
|
|
94
|
+
|
|
76
95
|
@dataclass
|
|
77
96
|
class RunConfig:
|
|
78
97
|
host: str
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.78.1.
|
|
3
|
+
Version: 1.78.1.dev4
|
|
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.78.1.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.78.1.dev4
|
|
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
|
|
@@ -6,7 +6,7 @@ gooddata_eval/cli/main.py,sha256=ZZv9pViwV4h21LPMrBOUCH99-oyM9dd2AFpGj9NWEJI,344
|
|
|
6
6
|
gooddata_eval/core/__init__.py,sha256=U54lANKm34yjqm7dZL9KkGouPbwAaQWC9wiAmb5B5g4,32
|
|
7
7
|
gooddata_eval/core/_output.py,sha256=sFbNtKg1lx431jpb15jISLlOXGTZ-r_KodfaTVyka6I,580
|
|
8
8
|
gooddata_eval/core/_retry.py,sha256=86ZPWu6ahCYC2GLvcbwjHTtj2RrGPOuxIFlCbtmNpvw,2990
|
|
9
|
-
gooddata_eval/core/config.py,sha256=
|
|
9
|
+
gooddata_eval/core/config.py,sha256=hla5t294uHC5Q9bzrhPjsdkwEcetLsEH-GqGAhs94b4,4823
|
|
10
10
|
gooddata_eval/core/connection.py,sha256=mh-Do5-Ti0-DXXikfCIH10eTTUbKuZUAS_sLoPNDJ5I,1229
|
|
11
11
|
gooddata_eval/core/granularity.py,sha256=CbniwgxtVj0oyf41UjeeZbBzOpeisSBlAfdkL79e6O0,3932
|
|
12
12
|
gooddata_eval/core/interaction.py,sha256=PUaJ1zdMwXBKjoNXNFXVgt_b8_i0LWdbMZndEQ3F6UM,2093
|
|
@@ -17,17 +17,17 @@ gooddata_eval/core/scoring.py,sha256=buY7ZoO08Dt59m2PPhFeNOyix5yYlf4mVxFyOn-aibk
|
|
|
17
17
|
gooddata_eval/core/timing.py,sha256=1grXpr48OH4GcAmeobhjwnRWqoMJwcjj-LLpHdS_raY,7789
|
|
18
18
|
gooddata_eval/core/workspace.py,sha256=S8exiy5gpDtcRtT8-i2fIRALF34Pbo0J46xm8X_ldus,11846
|
|
19
19
|
gooddata_eval/core/agentic/__init__.py,sha256=i2g22ousTYXi5FZeN5X0WVpa67RQXThwli8KcYfIYxM,4432
|
|
20
|
-
gooddata_eval/core/agentic/_catalog.py,sha256=
|
|
20
|
+
gooddata_eval/core/agentic/_catalog.py,sha256=My4SP5DteSJlp2WOQ66iemJkX3ANmdqrsgANDEm6u10,4194
|
|
21
21
|
gooddata_eval/core/agentic/_conversation_context.py,sha256=43ryGOQ9kTLLrzRxBphUn_87290vTZQ6oqWLnzjKYbA,16270
|
|
22
22
|
gooddata_eval/core/agentic/_failed_runs.py,sha256=iXN80ORMrIKEhV1QKGbyEZPP2w__mLPSrkm3hh-dTwM,3048
|
|
23
23
|
gooddata_eval/core/agentic/_gate.py,sha256=irI4eWOCfmsbKd6l-HdpoAlnWkq2wjo4b5crGU_Er1o,2631
|
|
24
24
|
gooddata_eval/core/agentic/_langfuse.py,sha256=NWhPgpd2NkYmAPBiT8VCOd_cddhhsmDGmcWmsvyGLGs,29729
|
|
25
25
|
gooddata_eval/core/agentic/_trace_linker.py,sha256=mSY_7UDZU97r5qcaDxMoyPQ46944XzbzFbMRd74G87k,15760
|
|
26
|
-
gooddata_eval/core/agentic/alert_skill.py,sha256=
|
|
26
|
+
gooddata_eval/core/agentic/alert_skill.py,sha256=Me7YrAAGWG6iqzJR18JdhVsri4LG4J1itIUdzgboZU0,60105
|
|
27
27
|
gooddata_eval/core/agentic/anomaly_detection.py,sha256=H_rKx6fCX3S11HzGAQGhy092YMmGj4foFBmrx6U-LHE,27701
|
|
28
28
|
gooddata_eval/core/agentic/conversation.py,sha256=_OLlST1twnSPnOQGfuuFCFKu92uwLVh2YBBA_nOW-5w,60004
|
|
29
29
|
gooddata_eval/core/agentic/dashboard_skill.py,sha256=PnFq1JjaMpAjQkzgklRkEFFKEs4WWhORikDVw5qjbaE,84630
|
|
30
|
-
gooddata_eval/core/agentic/document_skill.py,sha256=
|
|
30
|
+
gooddata_eval/core/agentic/document_skill.py,sha256=Uukqc6voa3lXbcSVgfzQyPH_kEFu3jrbrN9EDIMIuYA,45231
|
|
31
31
|
gooddata_eval/core/agentic/general_question.py,sha256=pOVYlJF9eKZwTqdPVsrH4X1OC-tXI7CMCFDdPNdBIMw,17758
|
|
32
32
|
gooddata_eval/core/agentic/guardrail.py,sha256=1Yz5QVVWo5GCOA8OKSy5efeysaaXB3bXcr_BT-Qu8tY,16398
|
|
33
33
|
gooddata_eval/core/agentic/kda_skill.py,sha256=2mav9itclavID28U4fORSR9p-iVTrddMyTHLKh8HxdE,26154
|
|
@@ -38,7 +38,7 @@ gooddata_eval/core/agentic/visualization.py,sha256=o4RVS-nRCVbVXjbE_jHuGzgW8x3hR
|
|
|
38
38
|
gooddata_eval/core/agentic/what_if.py,sha256=v5u9Yl45QGpVD2IHvPhVW_XOixI10IK4N-5_zP9Yymw,26365
|
|
39
39
|
gooddata_eval/core/chat/__init__.py,sha256=U54lANKm34yjqm7dZL9KkGouPbwAaQWC9wiAmb5B5g4,32
|
|
40
40
|
gooddata_eval/core/chat/render.py,sha256=CqsKN8__DlaiJj_bmcPZ1poc5LfUqtz7EzKRDbks4gU,2082
|
|
41
|
-
gooddata_eval/core/chat/sse_client.py,sha256=
|
|
41
|
+
gooddata_eval/core/chat/sse_client.py,sha256=yGUbWg7gr7ycVfEpvpmGUCIQ38nW0jiMmuJB9_va4UY,31921
|
|
42
42
|
gooddata_eval/core/dataset/__init__.py,sha256=U54lANKm34yjqm7dZL9KkGouPbwAaQWC9wiAmb5B5g4,32
|
|
43
43
|
gooddata_eval/core/dataset/from_insights.py,sha256=2eYh_84070t2BMeW2XPLAwzrJs7vKbFYMeCppvkDiEo,40880
|
|
44
44
|
gooddata_eval/core/dataset/langfuse_source.py,sha256=II-6fenEPhdyfckrL0iVdJpavjv67wUvy7OIuvHntrg,8697
|
|
@@ -77,8 +77,8 @@ gooddata_eval/core/summary/http_client.py,sha256=Umae42fiiJQ0e4hv63NRROHYvcFAGgX
|
|
|
77
77
|
gooddata_eval/core/takeaway/__init__.py,sha256=U54lANKm34yjqm7dZL9KkGouPbwAaQWC9wiAmb5B5g4,32
|
|
78
78
|
gooddata_eval/core/takeaway/evaluate.py,sha256=ei7DLSlG_Ove_R551Ofdw8WISf178XknU58jBWxB_is,11585
|
|
79
79
|
gooddata_eval/core/takeaway/http_client.py,sha256=yc3FNPArjfcSdAE7naRf7g_aW7BbJhwsfSEvAFd67Ik,4141
|
|
80
|
-
gooddata_eval-1.78.1.
|
|
81
|
-
gooddata_eval-1.78.1.
|
|
82
|
-
gooddata_eval-1.78.1.
|
|
83
|
-
gooddata_eval-1.78.1.
|
|
84
|
-
gooddata_eval-1.78.1.
|
|
80
|
+
gooddata_eval-1.78.1.dev4.dist-info/METADATA,sha256=uQZy3GjLJ3V1W9B9RR9kX6fLzD5-Var8lEQApCzH3iA,60351
|
|
81
|
+
gooddata_eval-1.78.1.dev4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
82
|
+
gooddata_eval-1.78.1.dev4.dist-info/entry_points.txt,sha256=28nFp5Viknx4haPYgzK9QHlwPFfC6rPF6RmjC2ht8EI,56
|
|
83
|
+
gooddata_eval-1.78.1.dev4.dist-info/licenses/LICENSE.txt,sha256=LVfVlC9maU3K9lgMwdZnClQQsOIbOekdcdVKr9qzJtk,256836
|
|
84
|
+
gooddata_eval-1.78.1.dev4.dist-info/RECORD,,
|
|
File without changes
|
{gooddata_eval-1.78.1.dev2.dist-info → gooddata_eval-1.78.1.dev4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{gooddata_eval-1.78.1.dev2.dist-info → gooddata_eval-1.78.1.dev4.dist-info}/licenses/LICENSE.txt
RENAMED
|
File without changes
|