gooddata-eval 1.68.1.dev2__py3-none-any.whl → 1.68.1.dev3__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/chat/sse_client.py +104 -11
- {gooddata_eval-1.68.1.dev2.dist-info → gooddata_eval-1.68.1.dev3.dist-info}/METADATA +2 -2
- {gooddata_eval-1.68.1.dev2.dist-info → gooddata_eval-1.68.1.dev3.dist-info}/RECORD +6 -6
- {gooddata_eval-1.68.1.dev2.dist-info → gooddata_eval-1.68.1.dev3.dist-info}/WHEEL +0 -0
- {gooddata_eval-1.68.1.dev2.dist-info → gooddata_eval-1.68.1.dev3.dist-info}/entry_points.txt +0 -0
- {gooddata_eval-1.68.1.dev2.dist-info → gooddata_eval-1.68.1.dev3.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -14,15 +14,88 @@ protocol, not on this class.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
17
20
|
from dataclasses import dataclass, field
|
|
18
|
-
from typing import Any, Iterable
|
|
21
|
+
from typing import Any, Callable, Iterable, TypeVar
|
|
19
22
|
|
|
20
23
|
import httpx
|
|
21
24
|
|
|
22
25
|
from gooddata_eval.core.models import ChatResult, DatasetItem
|
|
23
26
|
|
|
27
|
+
_log = logging.getLogger(__name__)
|
|
28
|
+
|
|
24
29
|
SSE_DATA_PREFIX = "data: "
|
|
25
30
|
|
|
31
|
+
_RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({429, 502, 503, 504})
|
|
32
|
+
_METADATA_SYNC_MARKER = "METADATA_SYNC_IN_PROGRESS"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ChatError(RuntimeError):
|
|
36
|
+
"""Non-retryable error reported by the chat SSE stream."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str, *, status_code: int | None = None, detail: str | None = None) -> None:
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
self.status_code = status_code
|
|
41
|
+
self.detail = detail
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TransientChatError(ChatError):
|
|
45
|
+
"""Retryable transient error: gen-ai temporarily unavailable or still syncing metadata."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _int_env(name: str, default: int) -> int:
|
|
49
|
+
"""Read an int from the environment, falling back to ``default`` when unset or blank."""
|
|
50
|
+
raw = os.getenv(name)
|
|
51
|
+
return int(raw) if raw else default
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _float_env(name: str, default: float) -> float:
|
|
55
|
+
"""Read a float from the environment, falling back to ``default`` when unset or blank."""
|
|
56
|
+
raw = os.getenv(name)
|
|
57
|
+
return float(raw) if raw else default
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Retry budget. Defaults give a ~2 min worst-case cap per send (5/10/20/40/60s);
|
|
61
|
+
# overridable via env so CI can retune without cutting a new gooddata-eval release.
|
|
62
|
+
_MAX_RETRIES = _int_env("GOODDATA_EVAL_CHAT_MAX_RETRIES", 5)
|
|
63
|
+
_INITIAL_BACKOFF_S = _float_env("GOODDATA_EVAL_CHAT_INITIAL_BACKOFF_S", 5.0)
|
|
64
|
+
_BACKOFF_FACTOR = _float_env("GOODDATA_EVAL_CHAT_BACKOFF_FACTOR", 2.0)
|
|
65
|
+
_MAX_BACKOFF_S = _float_env("GOODDATA_EVAL_CHAT_MAX_BACKOFF_S", 60.0)
|
|
66
|
+
|
|
67
|
+
T = TypeVar("T")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_retryable_exc(exc: Exception) -> bool:
|
|
71
|
+
if isinstance(exc, TransientChatError):
|
|
72
|
+
return True
|
|
73
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
74
|
+
return exc.response.status_code in _RETRYABLE_STATUS_CODES
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _retry_transient(operation: Callable[[], T], *, is_retryable: Callable[[Exception], bool]) -> T:
|
|
79
|
+
"""Run ``operation``; retry retryable failures with bounded exponential backoff."""
|
|
80
|
+
delay = _INITIAL_BACKOFF_S
|
|
81
|
+
for attempt in range(_MAX_RETRIES + 1): # 0..N => N retries + 1 initial attempt
|
|
82
|
+
try:
|
|
83
|
+
return operation()
|
|
84
|
+
except Exception as exc: # noqa: PERF203 — retry loop: per-attempt try/except is intentional
|
|
85
|
+
if attempt == _MAX_RETRIES or not is_retryable(exc):
|
|
86
|
+
raise
|
|
87
|
+
sleep_s = min(delay, _MAX_BACKOFF_S)
|
|
88
|
+
_log.warning(
|
|
89
|
+
"Transient gen-ai error (attempt %d/%d): %s; retrying in %.0fs",
|
|
90
|
+
attempt + 1,
|
|
91
|
+
_MAX_RETRIES + 1,
|
|
92
|
+
exc,
|
|
93
|
+
sleep_s,
|
|
94
|
+
)
|
|
95
|
+
time.sleep(sleep_s)
|
|
96
|
+
delay *= _BACKOFF_FACTOR
|
|
97
|
+
raise AssertionError("unreachable") # loop either returns or raises
|
|
98
|
+
|
|
26
99
|
|
|
27
100
|
@dataclass
|
|
28
101
|
class _SseAccumulator:
|
|
@@ -114,12 +187,23 @@ def parse_sse_lines(lines: Iterable[str]) -> ChatResult:
|
|
|
114
187
|
if not line or line.startswith("event: ") or not line.startswith(SSE_DATA_PREFIX):
|
|
115
188
|
continue
|
|
116
189
|
data_str = line[len(SSE_DATA_PREFIX) :]
|
|
190
|
+
if _METADATA_SYNC_MARKER in data_str:
|
|
191
|
+
raise TransientChatError(
|
|
192
|
+
f"SSE transient error: {_METADATA_SYNC_MARKER}",
|
|
193
|
+
status_code=None,
|
|
194
|
+
detail=None,
|
|
195
|
+
)
|
|
117
196
|
try:
|
|
118
197
|
event_data = json.loads(data_str)
|
|
119
198
|
except json.JSONDecodeError:
|
|
120
199
|
continue
|
|
121
200
|
if "statusCode" in event_data:
|
|
122
|
-
|
|
201
|
+
code = event_data.get("statusCode")
|
|
202
|
+
detail = event_data.get("detail")
|
|
203
|
+
message = f"SSE error {code}: {detail}"
|
|
204
|
+
if code in _RETRYABLE_STATUS_CODES:
|
|
205
|
+
raise TransientChatError(message, status_code=code, detail=detail)
|
|
206
|
+
raise ChatError(message, status_code=code, detail=detail)
|
|
123
207
|
item = event_data.get("item")
|
|
124
208
|
if not item:
|
|
125
209
|
continue
|
|
@@ -149,12 +233,17 @@ class ChatClient:
|
|
|
149
233
|
self._client = httpx.Client(timeout=timeout)
|
|
150
234
|
|
|
151
235
|
def create_conversation(self) -> str:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
236
|
+
def _do() -> str:
|
|
237
|
+
resp = self._client.post(self._base, headers={**self._auth, "Content-Type": "application/json"})
|
|
238
|
+
resp.raise_for_status()
|
|
239
|
+
body = resp.json()
|
|
240
|
+
if "conversationId" not in body:
|
|
241
|
+
raise ValueError(f"GoodData /chat/conversations response missing 'conversationId': {body}")
|
|
242
|
+
return body["conversationId"]
|
|
243
|
+
|
|
244
|
+
# NOTE: retrying create is not idempotent — a created-then-503 can leak an
|
|
245
|
+
# orphaned (ephemeral) conversation. Acceptable for eval; do not reuse blindly.
|
|
246
|
+
return _retry_transient(_do, is_retryable=_is_retryable_exc)
|
|
158
247
|
|
|
159
248
|
def delete_conversation(self, conversation_id: str) -> None:
|
|
160
249
|
try:
|
|
@@ -166,9 +255,13 @@ class ChatClient:
|
|
|
166
255
|
url = f"{self._base}/{conversation_id}/messages"
|
|
167
256
|
headers = {**self._auth, "Accept": "text/event-stream", "Content-Type": "application/json"}
|
|
168
257
|
body = {"item": {"role": "user", "content": {"type": "text", "text": question}}}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
258
|
+
|
|
259
|
+
def _do() -> ChatResult:
|
|
260
|
+
with self._client.stream("POST", url, json=body, headers=headers) as resp:
|
|
261
|
+
resp.raise_for_status()
|
|
262
|
+
return parse_sse_lines(resp.iter_lines())
|
|
263
|
+
|
|
264
|
+
return _retry_transient(_do, is_retryable=_is_retryable_exc)
|
|
172
265
|
|
|
173
266
|
def ask(self, item: DatasetItem) -> ChatResult:
|
|
174
267
|
"""Run one single-turn conversation: create, send, parse, clean up."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.68.1.
|
|
3
|
+
Version: 1.68.1.dev3
|
|
4
4
|
Summary: Evaluate the GoodData AI agent against your own questions and models.
|
|
5
5
|
Project-URL: Source, https://github.com/gooddata/gooddata-python-sdk
|
|
6
6
|
Author-email: GoodData <support@gooddata.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
17
17
|
Classifier: Topic :: Software Development
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist: gooddata-sdk~=1.68.1.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.68.1.dev3
|
|
21
21
|
Requires-Dist: httpx<1.0,>=0.27
|
|
22
22
|
Requires-Dist: orjson<4.0.0,>=3.9.15
|
|
23
23
|
Requires-Dist: pydantic<3.0,>=2.6
|
|
@@ -21,7 +21,7 @@ gooddata_eval/core/agentic/metric_skill.py,sha256=jFqVsxNEx-7IHa4v8aNLkDzD9OogDs
|
|
|
21
21
|
gooddata_eval/core/agentic/search_tool.py,sha256=EiWuzwdWUTY8H6wLST1rBB-fvc4meMazLKhiy0nl9K0,7634
|
|
22
22
|
gooddata_eval/core/agentic/visualization.py,sha256=ScSfx4-4lwHswqF_0ugOrIFu5d4tlAQf6CxaFVV9_mE,16743
|
|
23
23
|
gooddata_eval/core/chat/__init__.py,sha256=U54lANKm34yjqm7dZL9KkGouPbwAaQWC9wiAmb5B5g4,32
|
|
24
|
-
gooddata_eval/core/chat/sse_client.py,sha256=
|
|
24
|
+
gooddata_eval/core/chat/sse_client.py,sha256=1FVi2HWf2-uHMro7uWSiTiQTMEbh6WhVvQW8flnxymY,11205
|
|
25
25
|
gooddata_eval/core/dataset/__init__.py,sha256=U54lANKm34yjqm7dZL9KkGouPbwAaQWC9wiAmb5B5g4,32
|
|
26
26
|
gooddata_eval/core/dataset/langfuse_source.py,sha256=R2KzNx6LxAUtDZwHL5_UW5CJ6q-QnQGW7AhLZKBTDKU,5589
|
|
27
27
|
gooddata_eval/core/dataset/local.py,sha256=qoFFtRx_bNwlT1U37o8CKkSPwCad3JpmHPcNRuGSbjE,1233
|
|
@@ -44,8 +44,8 @@ gooddata_eval/core/reporting/console.py,sha256=kCHt4sCSaSRh0ZWqaSEJOl6_Jgb6uiouS
|
|
|
44
44
|
gooddata_eval/core/reporting/json_report.py,sha256=NIA_PvnZ0PbJnXz_zZauxc1gGnzqlLD_ouVqWeNmjtw,2837
|
|
45
45
|
gooddata_eval/core/summary/__init__.py,sha256=U54lANKm34yjqm7dZL9KkGouPbwAaQWC9wiAmb5B5g4,32
|
|
46
46
|
gooddata_eval/core/summary/http_client.py,sha256=dPArJ6zoCu1W9xmYXFA1WMDNsSOkhn3gaCpYgCUp3gk,2174
|
|
47
|
-
gooddata_eval-1.68.1.
|
|
48
|
-
gooddata_eval-1.68.1.
|
|
49
|
-
gooddata_eval-1.68.1.
|
|
50
|
-
gooddata_eval-1.68.1.
|
|
51
|
-
gooddata_eval-1.68.1.
|
|
47
|
+
gooddata_eval-1.68.1.dev3.dist-info/METADATA,sha256=ZGte7olPAbOw-BM69NnUhCskaAP4lSQRwwFTnapqJJM,9513
|
|
48
|
+
gooddata_eval-1.68.1.dev3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
49
|
+
gooddata_eval-1.68.1.dev3.dist-info/entry_points.txt,sha256=28nFp5Viknx4haPYgzK9QHlwPFfC6rPF6RmjC2ht8EI,56
|
|
50
|
+
gooddata_eval-1.68.1.dev3.dist-info/licenses/LICENSE.txt,sha256=LVfVlC9maU3K9lgMwdZnClQQsOIbOekdcdVKr9qzJtk,256836
|
|
51
|
+
gooddata_eval-1.68.1.dev3.dist-info/RECORD,,
|
|
File without changes
|
{gooddata_eval-1.68.1.dev2.dist-info → gooddata_eval-1.68.1.dev3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{gooddata_eval-1.68.1.dev2.dist-info → gooddata_eval-1.68.1.dev3.dist-info}/licenses/LICENSE.txt
RENAMED
|
File without changes
|