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.
Files changed (48) hide show
  1. seekrai/__init__.py +0 -1
  2. seekrai/abstract/api_requestor.py +108 -251
  3. seekrai/abstract/response_parsing.py +99 -0
  4. seekrai/client.py +12 -0
  5. seekrai/filemanager.py +181 -3
  6. seekrai/resources/__init__.py +10 -0
  7. seekrai/resources/agents/__init__.py +13 -0
  8. seekrai/resources/agents/agent_inference.py +277 -0
  9. seekrai/resources/agents/agents.py +272 -0
  10. seekrai/resources/agents/threads.py +454 -0
  11. seekrai/resources/alignment.py +3 -9
  12. seekrai/resources/completions.py +3 -9
  13. seekrai/resources/deployments.py +4 -9
  14. seekrai/resources/embeddings.py +3 -9
  15. seekrai/resources/files.py +163 -48
  16. seekrai/resources/finetune.py +3 -9
  17. seekrai/resources/images.py +3 -5
  18. seekrai/resources/ingestion.py +173 -0
  19. seekrai/resources/models.py +35 -124
  20. seekrai/resources/projects.py +4 -9
  21. seekrai/resources/resource_base.py +10 -0
  22. seekrai/resources/vectordb.py +482 -0
  23. seekrai/types/__init__.py +87 -0
  24. seekrai/types/agents/__init__.py +89 -0
  25. seekrai/types/agents/agent.py +42 -0
  26. seekrai/types/agents/runs.py +117 -0
  27. seekrai/types/agents/threads.py +265 -0
  28. seekrai/types/agents/tools/__init__.py +16 -0
  29. seekrai/types/agents/tools/env_model_config.py +7 -0
  30. seekrai/types/agents/tools/schemas/__init__.py +8 -0
  31. seekrai/types/agents/tools/schemas/file_search.py +9 -0
  32. seekrai/types/agents/tools/schemas/file_search_env.py +11 -0
  33. seekrai/types/agents/tools/tool.py +14 -0
  34. seekrai/types/agents/tools/tool_env_types.py +4 -0
  35. seekrai/types/agents/tools/tool_types.py +10 -0
  36. seekrai/types/alignment.py +6 -2
  37. seekrai/types/common.py +7 -2
  38. seekrai/types/files.py +5 -0
  39. seekrai/types/finetune.py +1 -0
  40. seekrai/types/ingestion.py +29 -0
  41. seekrai/types/models.py +3 -0
  42. seekrai/types/vectordb.py +78 -0
  43. {seekrai-0.4.2.dist-info → seekrai-0.5.0.dist-info}/METADATA +3 -3
  44. seekrai-0.5.0.dist-info/RECORD +67 -0
  45. {seekrai-0.4.2.dist-info → seekrai-0.5.0.dist-info}/WHEEL +1 -1
  46. seekrai-0.4.2.dist-info/RECORD +0 -46
  47. {seekrai-0.4.2.dist-info → seekrai-0.5.0.dist-info}/LICENSE +0 -0
  48. {seekrai-0.4.2.dist-info → seekrai-0.5.0.dist-info}/entry_points.txt +0 -0
seekrai/__init__.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from contextvars import ContextVar
4
3
  from typing import TYPE_CHECKING, Callable
5
4
 
6
5
  from seekrai import (
@@ -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 parse_stream_helper(line: str) -> str | None:
57
- if line and line.startswith("data:"):
58
- if line.startswith("data: "):
59
- # SSE event may be valid when it contains whitespace
60
- line = line[len("data: ") :]
61
- else:
62
- line = line[len("data:") :]
63
- if line.strip() == "[DONE]":
64
- # return here will cause GeneratorExit exception in urllib3
65
- # and it will close http connection with TCP Reset
66
- return None
67
- else:
68
- return line
69
- return None
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
- async def parse_stream_async(
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
- resp = self._interpret_response(result, stream)
195
- return resp, stream, self.api_key
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[SeekrFlowResponse | AsyncGenerator[SeekrFlowResponse, None], bool, str]:
232
- pass
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
- return resp, stream, self.api_key
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
- assert isinstance(resp.data, dict)
291
- error_resp = resp.data.get("detail")
292
- assert isinstance(
293
- error_resp, dict
294
- ), f"Unexpected error response {error_resp}"
295
- error_data = SeekrFlowErrorResponse(**(error_resp))
296
- except (KeyError, TypeError):
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
- # Rate limits were previously coded as 400's with code 'rate_limit'
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() == "get" or options.method.lower() == "delete":
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
- "assistance." % (options.method,)
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)