seekrai 0.4.2__py3-none-any.whl → 0.5.0__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.
- seekrai/__init__.py +0 -1
- seekrai/abstract/api_requestor.py +108 -251
- seekrai/abstract/response_parsing.py +99 -0
- seekrai/client.py +12 -0
- seekrai/filemanager.py +181 -3
- seekrai/resources/__init__.py +10 -0
- seekrai/resources/agents/__init__.py +13 -0
- seekrai/resources/agents/agent_inference.py +277 -0
- seekrai/resources/agents/agents.py +272 -0
- seekrai/resources/agents/threads.py +454 -0
- seekrai/resources/alignment.py +3 -9
- seekrai/resources/completions.py +3 -9
- seekrai/resources/deployments.py +4 -9
- seekrai/resources/embeddings.py +3 -9
- seekrai/resources/files.py +163 -48
- seekrai/resources/finetune.py +3 -9
- seekrai/resources/images.py +3 -5
- seekrai/resources/ingestion.py +173 -0
- seekrai/resources/models.py +35 -124
- seekrai/resources/projects.py +4 -9
- seekrai/resources/resource_base.py +10 -0
- seekrai/resources/vectordb.py +482 -0
- seekrai/types/__init__.py +87 -0
- seekrai/types/agents/__init__.py +89 -0
- seekrai/types/agents/agent.py +42 -0
- seekrai/types/agents/runs.py +117 -0
- seekrai/types/agents/threads.py +265 -0
- seekrai/types/agents/tools/__init__.py +16 -0
- seekrai/types/agents/tools/env_model_config.py +7 -0
- seekrai/types/agents/tools/schemas/__init__.py +8 -0
- seekrai/types/agents/tools/schemas/file_search.py +9 -0
- seekrai/types/agents/tools/schemas/file_search_env.py +11 -0
- seekrai/types/agents/tools/tool.py +14 -0
- seekrai/types/agents/tools/tool_env_types.py +4 -0
- seekrai/types/agents/tools/tool_types.py +10 -0
- seekrai/types/alignment.py +6 -2
- seekrai/types/common.py +7 -2
- seekrai/types/files.py +5 -0
- seekrai/types/finetune.py +1 -0
- seekrai/types/ingestion.py +29 -0
- seekrai/types/models.py +3 -0
- seekrai/types/vectordb.py +78 -0
- {seekrai-0.4.2.dist-info → seekrai-0.5.0.dist-info}/METADATA +3 -3
- seekrai-0.5.0.dist-info/RECORD +67 -0
- {seekrai-0.4.2.dist-info → seekrai-0.5.0.dist-info}/WHEEL +1 -1
- seekrai-0.4.2.dist-info/RECORD +0 -46
- {seekrai-0.4.2.dist-info → seekrai-0.5.0.dist-info}/LICENSE +0 -0
- {seekrai-0.4.2.dist-info → seekrai-0.5.0.dist-info}/entry_points.txt +0 -0
seekrai/__init__.py
CHANGED
|
@@ -10,15 +10,20 @@ from random import random
|
|
|
10
10
|
from typing import (
|
|
11
11
|
Any,
|
|
12
12
|
AsyncGenerator,
|
|
13
|
-
AsyncIterator,
|
|
14
13
|
Dict,
|
|
15
14
|
Iterator,
|
|
16
15
|
Mapping,
|
|
16
|
+
Optional,
|
|
17
17
|
Tuple,
|
|
18
18
|
overload,
|
|
19
19
|
)
|
|
20
20
|
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
21
21
|
|
|
22
|
+
from seekrai.abstract.response_parsing import (
|
|
23
|
+
parse_raw_response,
|
|
24
|
+
parse_raw_response_async,
|
|
25
|
+
)
|
|
26
|
+
|
|
22
27
|
|
|
23
28
|
if sys.version_info >= (3, 8):
|
|
24
29
|
from typing import Literal
|
|
@@ -37,7 +42,6 @@ from seekrai.constants import (
|
|
|
37
42
|
)
|
|
38
43
|
from seekrai.seekrflow_response import SeekrFlowResponse
|
|
39
44
|
from seekrai.types import SeekrFlowClient, SeekrFlowRequest
|
|
40
|
-
from seekrai.types.error import SeekrFlowErrorResponse
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
# Has one attribute per thread, 'session'.
|
|
@@ -46,43 +50,82 @@ _thread_context = threading.local()
|
|
|
46
50
|
|
|
47
51
|
def _build_api_url(url: str, query: str) -> str:
|
|
48
52
|
scheme, netloc, path, base_query, fragment = urlsplit(url)
|
|
49
|
-
|
|
50
53
|
if base_query:
|
|
51
54
|
query = "%s&%s" % (base_query, query)
|
|
52
|
-
|
|
53
55
|
return str(urlunsplit((scheme, netloc, path, query, fragment)))
|
|
54
56
|
|
|
55
57
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
def check_response_edge_cases(result: httpx.Response) -> Optional[SeekrFlowResponse]:
|
|
59
|
+
"""Logs, Raises, or Returns any specially-handled HTTP responses."""
|
|
60
|
+
headers = dict(result.headers.items())
|
|
61
|
+
request_id = headers.get("cf-ray")
|
|
62
|
+
code = result.status_code
|
|
63
|
+
if 500 <= code < 600 or code == 429:
|
|
64
|
+
utils.log_debug(
|
|
65
|
+
f"Encountered httpx.HTTPError. Error code: {result.status_code}"
|
|
66
|
+
)
|
|
67
|
+
if code >= 500:
|
|
68
|
+
raise httpx.HTTPError("Error communicating with API: {}".format(result))
|
|
69
|
+
if code == 204:
|
|
70
|
+
return SeekrFlowResponse({}, headers)
|
|
71
|
+
if code == 503:
|
|
72
|
+
raise error.ServiceUnavailableError(
|
|
73
|
+
"The server is overloaded or not ready yet.",
|
|
74
|
+
http_status=code,
|
|
75
|
+
headers=headers,
|
|
76
|
+
)
|
|
77
|
+
if code == 429:
|
|
78
|
+
raise error.RateLimitError(
|
|
79
|
+
result.read().decode("utf-8"),
|
|
80
|
+
http_status=code,
|
|
81
|
+
headers=headers,
|
|
82
|
+
request_id=request_id,
|
|
83
|
+
)
|
|
84
|
+
elif code in [400, 403, 404, 415]:
|
|
85
|
+
raise error.InvalidRequestError(
|
|
86
|
+
result.read().decode("utf-8"),
|
|
87
|
+
http_status=code,
|
|
88
|
+
headers=headers,
|
|
89
|
+
request_id=request_id,
|
|
90
|
+
)
|
|
91
|
+
elif code == 401:
|
|
92
|
+
raise error.AuthenticationError(
|
|
93
|
+
result.read().decode("utf-8"),
|
|
94
|
+
http_status=code,
|
|
95
|
+
headers=headers,
|
|
96
|
+
request_id=request_id,
|
|
97
|
+
)
|
|
98
|
+
if not 200 <= code < 300:
|
|
99
|
+
utils.log_info(
|
|
100
|
+
"SeekrFlow API error received",
|
|
101
|
+
error_code=code,
|
|
102
|
+
error_message=result.read(),
|
|
103
|
+
)
|
|
104
|
+
raise error.APIError(
|
|
105
|
+
result.content.decode("utf-8"),
|
|
106
|
+
http_status=code,
|
|
107
|
+
headers=headers,
|
|
108
|
+
request_id=headers.get("cf-ray"),
|
|
109
|
+
)
|
|
110
|
+
if 300 < code < 500:
|
|
111
|
+
raise httpx.HTTPError(result.read().decode())
|
|
70
112
|
|
|
113
|
+
return None # No errors, no special-case response
|
|
71
114
|
|
|
72
|
-
def parse_stream(rbody: Iterator[str]) -> Iterator[str]:
|
|
73
|
-
for line in rbody:
|
|
74
|
-
_line = parse_stream_helper(line)
|
|
75
|
-
if _line is not None:
|
|
76
|
-
yield _line
|
|
77
115
|
|
|
116
|
+
async def acheck_response_edge_cases(
|
|
117
|
+
result: httpx.Response,
|
|
118
|
+
) -> Optional[SeekrFlowResponse]:
|
|
119
|
+
"""Same as check_response_edge_cases(), but async."""
|
|
120
|
+
if result.status_code != 200: # Only synchronize for errors
|
|
121
|
+
synced_result = httpx.Response(
|
|
122
|
+
status_code=result.status_code,
|
|
123
|
+
headers=result.headers,
|
|
124
|
+
content=await result.aread(),
|
|
125
|
+
)
|
|
126
|
+
return check_response_edge_cases(synced_result)
|
|
78
127
|
|
|
79
|
-
|
|
80
|
-
rbody: AsyncIterator[str],
|
|
81
|
-
) -> AsyncGenerator[str, Any]:
|
|
82
|
-
async for line in rbody:
|
|
83
|
-
_line = parse_stream_helper(line)
|
|
84
|
-
if _line is not None:
|
|
85
|
-
yield _line
|
|
128
|
+
return None # No errors, no special-case response. Carry on asynchronously...
|
|
86
129
|
|
|
87
130
|
|
|
88
131
|
class APIRequestor:
|
|
@@ -107,29 +150,19 @@ class APIRequestor:
|
|
|
107
150
|
"""
|
|
108
151
|
if response_headers is None:
|
|
109
152
|
return None
|
|
110
|
-
|
|
111
|
-
# First, try the non-standard `retry-after-ms` header for milliseconds,
|
|
112
|
-
# which is more precise than integer-seconds `retry-after`
|
|
113
153
|
try:
|
|
114
154
|
retry_ms_header = response_headers.get("retry-after-ms", None)
|
|
115
155
|
return float(retry_ms_header) / 1000
|
|
116
156
|
except (TypeError, ValueError):
|
|
117
157
|
pass
|
|
118
|
-
|
|
119
|
-
# Next, try parsing `retry-after` header as seconds (allowing nonstandard floats).
|
|
120
158
|
retry_header = str(response_headers.get("retry-after"))
|
|
121
159
|
try:
|
|
122
|
-
# note: the spec indicates that this should only ever be an integer
|
|
123
|
-
# but if someone sends a float there's no reason for us to not respect it
|
|
124
160
|
return float(retry_header)
|
|
125
161
|
except (TypeError, ValueError):
|
|
126
162
|
pass
|
|
127
|
-
|
|
128
|
-
# Last, try parsing `retry-after` as a date.
|
|
129
163
|
retry_date_tuple = email.utils.parsedate_tz(retry_header)
|
|
130
164
|
if retry_date_tuple is None:
|
|
131
165
|
return None
|
|
132
|
-
|
|
133
166
|
retry_date = email.utils.mktime_tz(retry_date_tuple)
|
|
134
167
|
return float(retry_date - time.time())
|
|
135
168
|
|
|
@@ -138,17 +171,11 @@ class APIRequestor:
|
|
|
138
171
|
remaining_retries: int,
|
|
139
172
|
response_headers: Dict[str, Any] | None = None,
|
|
140
173
|
) -> float:
|
|
141
|
-
# If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says.
|
|
142
174
|
retry_after = self._parse_retry_after_header(response_headers)
|
|
143
175
|
if retry_after is not None and 0 < retry_after <= 60:
|
|
144
176
|
return retry_after
|
|
145
|
-
|
|
146
177
|
nb_retries = self.retries - remaining_retries
|
|
147
|
-
|
|
148
|
-
# Apply exponential backoff, but not more than the max.
|
|
149
178
|
sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY)
|
|
150
|
-
|
|
151
|
-
# Apply some jitter, plus-or-minus half a second.
|
|
152
179
|
jitter = 1 - 0.25 * random()
|
|
153
180
|
timeout = sleep_seconds * jitter
|
|
154
181
|
return timeout if timeout >= 0 else 0
|
|
@@ -159,26 +186,21 @@ class APIRequestor:
|
|
|
159
186
|
options: SeekrFlowRequest,
|
|
160
187
|
stream: Literal[True],
|
|
161
188
|
request_timeout: float | None = ...,
|
|
162
|
-
) -> Tuple[Iterator[SeekrFlowResponse], bool, str]:
|
|
163
|
-
pass
|
|
164
|
-
|
|
189
|
+
) -> Tuple[Iterator[SeekrFlowResponse], bool, str]: ...
|
|
165
190
|
@overload
|
|
166
191
|
def request(
|
|
167
192
|
self,
|
|
168
193
|
options: SeekrFlowRequest,
|
|
169
194
|
stream: Literal[False] = ...,
|
|
170
195
|
request_timeout: float | None = ...,
|
|
171
|
-
) -> Tuple[SeekrFlowResponse, bool, str]:
|
|
172
|
-
pass
|
|
173
|
-
|
|
196
|
+
) -> Tuple[SeekrFlowResponse, bool, str]: ...
|
|
174
197
|
@overload
|
|
175
198
|
def request(
|
|
176
199
|
self,
|
|
177
200
|
options: SeekrFlowRequest,
|
|
178
201
|
stream: bool = ...,
|
|
179
202
|
request_timeout: float | None = ...,
|
|
180
|
-
) -> Tuple[SeekrFlowResponse | Iterator[SeekrFlowResponse], bool, str]:
|
|
181
|
-
pass
|
|
203
|
+
) -> Tuple[SeekrFlowResponse | Iterator[SeekrFlowResponse], bool, str]: ...
|
|
182
204
|
|
|
183
205
|
def request(
|
|
184
206
|
self,
|
|
@@ -191,8 +213,19 @@ class APIRequestor:
|
|
|
191
213
|
str | None,
|
|
192
214
|
]:
|
|
193
215
|
result = self.request_raw(options, stream, request_timeout)
|
|
194
|
-
|
|
195
|
-
|
|
216
|
+
|
|
217
|
+
special_case = check_response_edge_cases(result)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
response = special_case or parse_raw_response(result, stream)
|
|
221
|
+
except (JSONDecodeError, UnicodeDecodeError, ValueError) as e:
|
|
222
|
+
raise error.APIError(
|
|
223
|
+
f"Error code: {result.status_code} - {result.content!r}",
|
|
224
|
+
http_status=result.status_code,
|
|
225
|
+
headers=result.headers,
|
|
226
|
+
) from e
|
|
227
|
+
|
|
228
|
+
return response, stream, self.api_key
|
|
196
229
|
|
|
197
230
|
@overload
|
|
198
231
|
async def arequest(
|
|
@@ -200,9 +233,7 @@ class APIRequestor:
|
|
|
200
233
|
options: SeekrFlowRequest,
|
|
201
234
|
stream: Literal[True],
|
|
202
235
|
request_timeout: float | None = ...,
|
|
203
|
-
) -> Tuple[AsyncGenerator[SeekrFlowResponse, None], bool, str]:
|
|
204
|
-
pass
|
|
205
|
-
|
|
236
|
+
) -> Tuple[AsyncGenerator[SeekrFlowResponse, None], bool, str]: ...
|
|
206
237
|
@overload
|
|
207
238
|
async def arequest(
|
|
208
239
|
self,
|
|
@@ -210,26 +241,23 @@ class APIRequestor:
|
|
|
210
241
|
*,
|
|
211
242
|
stream: Literal[True],
|
|
212
243
|
request_timeout: float | None = ...,
|
|
213
|
-
) -> Tuple[AsyncGenerator[SeekrFlowResponse, None], bool, str]:
|
|
214
|
-
pass
|
|
215
|
-
|
|
244
|
+
) -> Tuple[AsyncGenerator[SeekrFlowResponse, None], bool, str]: ...
|
|
216
245
|
@overload
|
|
217
246
|
async def arequest(
|
|
218
247
|
self,
|
|
219
248
|
options: SeekrFlowRequest,
|
|
220
249
|
stream: Literal[False] = ...,
|
|
221
250
|
request_timeout: float | None = ...,
|
|
222
|
-
) -> Tuple[SeekrFlowResponse, bool, str]:
|
|
223
|
-
pass
|
|
224
|
-
|
|
251
|
+
) -> Tuple[SeekrFlowResponse, bool, str]: ...
|
|
225
252
|
@overload
|
|
226
253
|
async def arequest(
|
|
227
254
|
self,
|
|
228
255
|
options: SeekrFlowRequest,
|
|
229
256
|
stream: bool = ...,
|
|
230
257
|
request_timeout: float | None = ...,
|
|
231
|
-
) -> Tuple[
|
|
232
|
-
|
|
258
|
+
) -> Tuple[
|
|
259
|
+
SeekrFlowResponse | AsyncGenerator[SeekrFlowResponse, None], bool, str
|
|
260
|
+
]: ...
|
|
233
261
|
|
|
234
262
|
async def arequest(
|
|
235
263
|
self,
|
|
@@ -253,21 +281,13 @@ class APIRequestor:
|
|
|
253
281
|
result = await client.send(req, stream=stream)
|
|
254
282
|
except httpx.TimeoutException as e:
|
|
255
283
|
utils.log_debug("Encountered httpx.TimeoutException")
|
|
256
|
-
|
|
257
284
|
raise error.Timeout("Request timed out: {}".format(e)) from e
|
|
258
285
|
except httpx.RequestError as e:
|
|
259
286
|
utils.log_debug("Encountered httpx.RequestError")
|
|
260
|
-
|
|
261
287
|
raise error.APIConnectionError(
|
|
262
288
|
"Error communicating with API: {}".format(e)
|
|
263
289
|
) from e
|
|
264
290
|
|
|
265
|
-
# retry on 5XX error or rate-limit
|
|
266
|
-
if 500 <= result.status_code < 600 or result.status_code == 429:
|
|
267
|
-
utils.log_debug(
|
|
268
|
-
f"Encountered httpx.HTTPError. Error code: {result.status_code}"
|
|
269
|
-
)
|
|
270
|
-
|
|
271
291
|
utils.log_debug(
|
|
272
292
|
"SeekrFlow API response",
|
|
273
293
|
path=abs_url,
|
|
@@ -275,79 +295,19 @@ class APIRequestor:
|
|
|
275
295
|
processing_ms=result.headers.get("x-total-time"),
|
|
276
296
|
request_id=result.headers.get("CF-RAY"),
|
|
277
297
|
)
|
|
278
|
-
resp = await self._interpret_async_response(result, stream)
|
|
279
298
|
|
|
280
|
-
|
|
299
|
+
special_case = await acheck_response_edge_cases(result)
|
|
281
300
|
|
|
282
|
-
@classmethod
|
|
283
|
-
def handle_error_response(
|
|
284
|
-
cls,
|
|
285
|
-
resp: SeekrFlowResponse,
|
|
286
|
-
rcode: int,
|
|
287
|
-
stream_error: bool = False,
|
|
288
|
-
) -> Exception:
|
|
289
301
|
try:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
raise error.JSONError(
|
|
298
|
-
"Invalid response object from API: %r (HTTP response code "
|
|
299
|
-
"was %d)" % (resp.data, rcode),
|
|
300
|
-
http_status=rcode,
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
utils.log_info(
|
|
304
|
-
"SeekrFlow API error received",
|
|
305
|
-
error_code=error_data.code,
|
|
306
|
-
error_type=error_data.type_,
|
|
307
|
-
error_message=error_data.message,
|
|
308
|
-
error_param=error_data.param,
|
|
309
|
-
stream_error=stream_error,
|
|
310
|
-
)
|
|
302
|
+
response = special_case or await parse_raw_response_async(result, stream)
|
|
303
|
+
except (JSONDecodeError, UnicodeDecodeError, ValueError) as e:
|
|
304
|
+
raise error.APIError(
|
|
305
|
+
f"Error code: {result.status_code} - {result.content!r}",
|
|
306
|
+
http_status=result.status_code,
|
|
307
|
+
headers=result.headers,
|
|
308
|
+
) from e
|
|
311
309
|
|
|
312
|
-
|
|
313
|
-
if rcode == 429:
|
|
314
|
-
return error.RateLimitError(
|
|
315
|
-
error_data,
|
|
316
|
-
http_status=rcode,
|
|
317
|
-
headers=resp._headers,
|
|
318
|
-
request_id=resp.request_id,
|
|
319
|
-
)
|
|
320
|
-
elif rcode in [400, 403, 404, 415]:
|
|
321
|
-
return error.InvalidRequestError(
|
|
322
|
-
error_data,
|
|
323
|
-
http_status=rcode,
|
|
324
|
-
headers=resp._headers,
|
|
325
|
-
request_id=resp.request_id,
|
|
326
|
-
)
|
|
327
|
-
elif rcode == 401:
|
|
328
|
-
return error.AuthenticationError(
|
|
329
|
-
error_data,
|
|
330
|
-
http_status=rcode,
|
|
331
|
-
headers=resp._headers,
|
|
332
|
-
request_id=resp.request_id,
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
elif stream_error:
|
|
336
|
-
parts = [error_data.message, "(Error occurred while streaming.)"]
|
|
337
|
-
message = " ".join([p for p in parts if p is not None])
|
|
338
|
-
return error.APIError(
|
|
339
|
-
message,
|
|
340
|
-
http_status=rcode,
|
|
341
|
-
headers=resp._headers,
|
|
342
|
-
request_id=resp.request_id,
|
|
343
|
-
)
|
|
344
|
-
else:
|
|
345
|
-
return error.APIError(
|
|
346
|
-
error_data,
|
|
347
|
-
http_status=rcode,
|
|
348
|
-
headers=resp._headers,
|
|
349
|
-
request_id=resp.request_id,
|
|
350
|
-
)
|
|
310
|
+
return response, stream, self.api_key
|
|
351
311
|
|
|
352
312
|
@classmethod
|
|
353
313
|
def _validate_headers(
|
|
@@ -356,20 +316,14 @@ class APIRequestor:
|
|
|
356
316
|
headers: Dict[str, str] = {}
|
|
357
317
|
if supplied_headers is None:
|
|
358
318
|
return headers
|
|
359
|
-
|
|
360
319
|
if not isinstance(supplied_headers, dict):
|
|
361
320
|
raise TypeError("Headers must be a dictionary")
|
|
362
|
-
|
|
363
321
|
for k, v in supplied_headers.items():
|
|
364
322
|
if not isinstance(k, str):
|
|
365
323
|
raise TypeError("Header keys must be strings")
|
|
366
324
|
if not isinstance(v, str):
|
|
367
325
|
raise TypeError("Header values must be strings")
|
|
368
326
|
headers[k] = v
|
|
369
|
-
|
|
370
|
-
# NOTE: It is possible to do more validation of the headers, but a request could always
|
|
371
|
-
# be made to the API manually with invalid headers, so we need to handle them server side.
|
|
372
|
-
|
|
373
327
|
return headers
|
|
374
328
|
|
|
375
329
|
def _prepare_request_raw(
|
|
@@ -379,29 +333,25 @@ class APIRequestor:
|
|
|
379
333
|
) -> Tuple[str, Dict[str, str], Mapping[str, Any] | None | str]:
|
|
380
334
|
abs_url = options.url if absolute else "%s%s" % (self.api_base, options.url)
|
|
381
335
|
headers = self._validate_headers(options.headers or self.supplied_headers)
|
|
382
|
-
|
|
383
336
|
data: Mapping[str, Any] | None | str = None
|
|
384
|
-
if options.method.lower()
|
|
337
|
+
if options.method.lower() in {"get", "delete"}:
|
|
385
338
|
if options.params:
|
|
386
339
|
encoded_params = urlencode(
|
|
387
340
|
[(k, v) for k, v in options.params.items() if v is not None]
|
|
388
341
|
)
|
|
389
342
|
abs_url = _build_api_url(abs_url, encoded_params)
|
|
390
|
-
elif options.method.lower() in {"post", "put"}:
|
|
343
|
+
elif options.method.lower() in {"post", "put", "patch"}:
|
|
391
344
|
data = options.params
|
|
392
345
|
if options.params and not options.files:
|
|
393
346
|
data = json.dumps(data)
|
|
394
|
-
|
|
395
347
|
else:
|
|
396
348
|
raise error.APIConnectionError(
|
|
397
349
|
"Unrecognized HTTP method %r. This may indicate a bug in the "
|
|
398
|
-
"SeekrFlow SDK. Please contact us by filling out https://www.seekrflow.ai/contact for "
|
|
399
|
-
|
|
350
|
+
"SeekrFlow SDK. Please contact us by filling out https://www.seekrflow.ai/contact for assistance."
|
|
351
|
+
% (options.method,)
|
|
400
352
|
)
|
|
401
|
-
|
|
402
353
|
if not options.override_headers:
|
|
403
354
|
headers = utils.get_headers(options.method, self.api_key, headers)
|
|
404
|
-
|
|
405
355
|
utils.log_debug(
|
|
406
356
|
"Request to SeekrFlow API",
|
|
407
357
|
method=options.method,
|
|
@@ -409,7 +359,6 @@ class APIRequestor:
|
|
|
409
359
|
post_data=data,
|
|
410
360
|
headers=json.dumps(headers),
|
|
411
361
|
)
|
|
412
|
-
|
|
413
362
|
return abs_url, headers, data
|
|
414
363
|
|
|
415
364
|
def request_raw(
|
|
@@ -432,21 +381,13 @@ class APIRequestor:
|
|
|
432
381
|
result = client.send(req, stream=stream)
|
|
433
382
|
except httpx.TimeoutException as e:
|
|
434
383
|
utils.log_debug("Encountered httpx.TimeoutException")
|
|
435
|
-
|
|
436
384
|
raise error.Timeout("Request timed out: {}".format(e)) from e
|
|
437
385
|
except httpx.RequestError as e:
|
|
438
386
|
utils.log_debug("Encountered httpx.HTTPError")
|
|
439
|
-
|
|
440
387
|
raise error.APIConnectionError(
|
|
441
388
|
"Error communicating with API: {}".format(e)
|
|
442
389
|
) from e
|
|
443
390
|
|
|
444
|
-
# retry on 5XX error or rate-limit
|
|
445
|
-
if result.status_code > 300 and result.status_code < 500:
|
|
446
|
-
raise httpx.HTTPError(result.content.decode())
|
|
447
|
-
elif result.status_code >= 500:
|
|
448
|
-
raise httpx.HTTPError("Error communicating with API: {}".format(result))
|
|
449
|
-
|
|
450
391
|
utils.log_debug(
|
|
451
392
|
"SeekrFlow API response",
|
|
452
393
|
path=abs_url,
|
|
@@ -455,87 +396,3 @@ class APIRequestor:
|
|
|
455
396
|
request_id=result.headers.get("CF-RAY"),
|
|
456
397
|
)
|
|
457
398
|
return result
|
|
458
|
-
|
|
459
|
-
def _interpret_response(
|
|
460
|
-
self, result: httpx.Response, stream: bool
|
|
461
|
-
) -> SeekrFlowResponse | Iterator[SeekrFlowResponse]:
|
|
462
|
-
"""Returns the response(s) and a bool indicating whether it is a stream."""
|
|
463
|
-
|
|
464
|
-
if stream and "text/event-stream" in result.headers.get("Content-Type", ""):
|
|
465
|
-
iterator = (
|
|
466
|
-
self._interpret_response_line(
|
|
467
|
-
line, result.status_code, result.headers, stream=True
|
|
468
|
-
)
|
|
469
|
-
for line in parse_stream(result.iter_text())
|
|
470
|
-
)
|
|
471
|
-
return iterator
|
|
472
|
-
else:
|
|
473
|
-
return self._interpret_response_line(
|
|
474
|
-
result.content.decode("utf-8"),
|
|
475
|
-
result.status_code,
|
|
476
|
-
result.headers,
|
|
477
|
-
stream=False,
|
|
478
|
-
)
|
|
479
|
-
|
|
480
|
-
async def _interpret_async_response(
|
|
481
|
-
self, result: httpx.Response, stream: bool
|
|
482
|
-
) -> AsyncGenerator[SeekrFlowResponse, None] | SeekrFlowResponse:
|
|
483
|
-
"""Returns the response(s) and a bool indicating whether it is a stream."""
|
|
484
|
-
if stream and "text/event-stream" in result.headers.get("Content-Type", ""):
|
|
485
|
-
iterator = (
|
|
486
|
-
self._interpret_response_line(
|
|
487
|
-
line, result.status_code, result.headers, stream=True
|
|
488
|
-
)
|
|
489
|
-
async for line in parse_stream_async(result.aiter_text())
|
|
490
|
-
)
|
|
491
|
-
return iterator
|
|
492
|
-
else:
|
|
493
|
-
try:
|
|
494
|
-
result.read()
|
|
495
|
-
except httpx.TimeoutException as e:
|
|
496
|
-
raise error.Timeout("Request timed out") from e
|
|
497
|
-
except httpx.HTTPError as e:
|
|
498
|
-
utils.log_warn(e, body=result.content)
|
|
499
|
-
return self._interpret_response_line(
|
|
500
|
-
(result.read()).decode("utf-8"),
|
|
501
|
-
result.status_code,
|
|
502
|
-
result.headers,
|
|
503
|
-
stream=False,
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
def _interpret_response_line(
|
|
507
|
-
self, rbody: str, rcode: int, rheaders: Any, stream: bool
|
|
508
|
-
) -> SeekrFlowResponse:
|
|
509
|
-
# HTTP 204 response code does not have any content in the body.
|
|
510
|
-
if rcode == 204:
|
|
511
|
-
return SeekrFlowResponse({}, rheaders)
|
|
512
|
-
|
|
513
|
-
if rcode == 503:
|
|
514
|
-
raise error.ServiceUnavailableError(
|
|
515
|
-
"The server is overloaded or not ready yet.",
|
|
516
|
-
http_status=rcode,
|
|
517
|
-
headers=rheaders,
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
try:
|
|
521
|
-
if "text/plain" in rheaders.get("Content-Type", ""):
|
|
522
|
-
data: Dict[str, Any] = {"message": rbody}
|
|
523
|
-
else:
|
|
524
|
-
if rbody.strip().endswith("[DONE]"):
|
|
525
|
-
# TODO
|
|
526
|
-
rbody = rbody.replace("data: [DONE]", "")
|
|
527
|
-
if rbody.startswith("data: "):
|
|
528
|
-
rbody = rbody[len("data: ") :]
|
|
529
|
-
data = json.loads(rbody)
|
|
530
|
-
except (JSONDecodeError, UnicodeDecodeError) as e:
|
|
531
|
-
raise error.APIError(
|
|
532
|
-
f"Error code: {rcode} -{rbody}",
|
|
533
|
-
http_status=rcode,
|
|
534
|
-
headers=rheaders,
|
|
535
|
-
) from e
|
|
536
|
-
resp = SeekrFlowResponse(data, rheaders)
|
|
537
|
-
|
|
538
|
-
# Handle streaming errors
|
|
539
|
-
if not 200 <= rcode < 300:
|
|
540
|
-
raise self.handle_error_response(resp, rcode, stream_error=stream)
|
|
541
|
-
return resp
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, AsyncGenerator, AsyncIterator, Iterator, Union
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from seekrai.seekrflow_response import SeekrFlowResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DATA_LINE_PATTERN = re.compile(r"\A(data:)?\s*(.*?)\s*(\[DONE\])?\s*\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_data_line(line: str) -> str:
|
|
14
|
+
parse = re.fullmatch(DATA_LINE_PATTERN, line)
|
|
15
|
+
if parse:
|
|
16
|
+
return parse.group(2)
|
|
17
|
+
else:
|
|
18
|
+
# This should never happen. Basically everything matches the regex.
|
|
19
|
+
raise ValueError(f"Line did not match expected format: {line}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_stream(chunks: Iterator[str]) -> Iterator[Any]:
|
|
23
|
+
buffer = []
|
|
24
|
+
for chunk in chunks:
|
|
25
|
+
content = parse_data_line(chunk)
|
|
26
|
+
|
|
27
|
+
if content:
|
|
28
|
+
buffer.append(content)
|
|
29
|
+
else:
|
|
30
|
+
yield json.loads("\n".join(buffer))
|
|
31
|
+
buffer = []
|
|
32
|
+
|
|
33
|
+
if buffer:
|
|
34
|
+
yield json.loads("\n".join(buffer))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def parse_stream_async(chunks: AsyncIterator[str]) -> AsyncIterator[Any]:
|
|
38
|
+
buffer = []
|
|
39
|
+
async for chunk in chunks:
|
|
40
|
+
content = parse_data_line(chunk)
|
|
41
|
+
|
|
42
|
+
if content:
|
|
43
|
+
buffer.append(content)
|
|
44
|
+
else:
|
|
45
|
+
yield json.loads("\n".join(buffer))
|
|
46
|
+
buffer = []
|
|
47
|
+
|
|
48
|
+
if buffer:
|
|
49
|
+
yield json.loads("\n".join(buffer))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_plain_content(content: bytes) -> dict[str, Any]:
|
|
53
|
+
return {"message": content.decode("utf-8")}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_complete_content(content: bytes) -> Any:
|
|
57
|
+
return json.loads(parse_data_line(content.decode("utf-8")))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_raw_response(
|
|
61
|
+
response: httpx.Response, stream: bool
|
|
62
|
+
) -> Union[SeekrFlowResponse, Iterator[SeekrFlowResponse]]:
|
|
63
|
+
headers = dict(response.headers.items())
|
|
64
|
+
content_type = headers.get("content-type", "")
|
|
65
|
+
|
|
66
|
+
if stream and "text/event-stream" in content_type:
|
|
67
|
+
stream_content = parse_stream(response.iter_lines())
|
|
68
|
+
return (SeekrFlowResponse(msg, headers) for msg in stream_content)
|
|
69
|
+
|
|
70
|
+
elif "text/plain" in content_type:
|
|
71
|
+
content = parse_plain_content(response.content)
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
content = parse_complete_content(response.content)
|
|
75
|
+
|
|
76
|
+
return SeekrFlowResponse(content, headers)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def parse_raw_response_async(
|
|
80
|
+
response: httpx.Response, stream: bool
|
|
81
|
+
) -> Union[SeekrFlowResponse, AsyncGenerator[SeekrFlowResponse, None]]:
|
|
82
|
+
headers = dict(response.headers.items())
|
|
83
|
+
content_type = headers.get("content-type", "")
|
|
84
|
+
|
|
85
|
+
if stream and "text/event-stream" in content_type:
|
|
86
|
+
|
|
87
|
+
async def generate_parse_stream() -> AsyncGenerator[SeekrFlowResponse, None]:
|
|
88
|
+
async for msg in parse_stream_async(response.aiter_lines()):
|
|
89
|
+
yield SeekrFlowResponse(msg, headers)
|
|
90
|
+
|
|
91
|
+
return generate_parse_stream()
|
|
92
|
+
|
|
93
|
+
elif "text/plain" in content_type:
|
|
94
|
+
content = parse_plain_content(response.read())
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
content = parse_complete_content(response.read())
|
|
98
|
+
|
|
99
|
+
return SeekrFlowResponse(content, headers)
|