openaivec 0.13.0__tar.gz → 0.13.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openaivec-0.13.0 → openaivec-0.13.1}/PKG-INFO +1 -1
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/embeddings.py +3 -3
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/responses.py +3 -3
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/util.py +18 -12
- openaivec-0.13.1/tests/test_util.py +291 -0
- openaivec-0.13.0/tests/test_util.py +0 -41
- {openaivec-0.13.0 → openaivec-0.13.1}/.env.example +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/.github/workflows/python-mkdocs.yml +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/.github/workflows/python-package.yml +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/.github/workflows/python-test.yml +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/.github/workflows/python-update.yml +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/.gitignore +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/CODE_OF_CONDUCT.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/LICENSE +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/README.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/SECURITY.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/SUPPORT.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/di.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/embeddings.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/pandas_ext.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/prompt.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/proxy.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/responses.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/spark.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/task.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/customer_support/customer_sentiment.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/customer_support/inquiry_classification.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/customer_support/inquiry_summary.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/customer_support/intent_analysis.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/customer_support/response_suggestion.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/customer_support/urgency_analysis.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/nlp/dependency_parsing.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/nlp/keyword_extraction.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/nlp/morphological_analysis.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/nlp/named_entity_recognition.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/nlp/sentiment_analysis.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/nlp/translation.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/api/util.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/index.md +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/docs/robots.txt +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/mkdocs.yml +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/pyproject.toml +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/__init__.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/di.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/log.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/model.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/pandas_ext.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/prompt.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/provider.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/proxy.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/serialize.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/spark.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/__init__.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/__init__.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/customer_sentiment.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/inquiry_classification.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/inquiry_summary.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/intent_analysis.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/response_suggestion.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/urgency_analysis.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/nlp/__init__.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/nlp/dependency_parsing.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/nlp/keyword_extraction.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/nlp/morphological_analysis.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/nlp/named_entity_recognition.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/nlp/sentiment_analysis.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/nlp/translation.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/table/__init__.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/table/fillna.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/__init__.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_di.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_embeddings.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_pandas_ext.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_prompt.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_provider.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_proxy.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_responses.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_serialize.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_spark.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/tests/test_task.py +0 -0
- {openaivec-0.13.0 → openaivec-0.13.1}/uv.lock +0 -0
|
@@ -4,7 +4,7 @@ from typing import List
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from numpy.typing import NDArray
|
|
7
|
-
from openai import AsyncOpenAI, OpenAI, RateLimitError
|
|
7
|
+
from openai import AsyncOpenAI, InternalServerError, OpenAI, RateLimitError
|
|
8
8
|
|
|
9
9
|
from .log import observe
|
|
10
10
|
from .proxy import AsyncBatchingMapProxy, BatchingMapProxy
|
|
@@ -47,7 +47,7 @@ class BatchEmbeddings:
|
|
|
47
47
|
return cls(client=client, model_name=model_name, cache=BatchingMapProxy(batch_size=batch_size))
|
|
48
48
|
|
|
49
49
|
@observe(_LOGGER)
|
|
50
|
-
@backoff(
|
|
50
|
+
@backoff(exceptions=[RateLimitError, InternalServerError], scale=1, max_retries=12)
|
|
51
51
|
def _embed_chunk(self, inputs: List[str]) -> List[NDArray[np.float32]]:
|
|
52
52
|
"""Embed one minibatch of strings.
|
|
53
53
|
|
|
@@ -155,7 +155,7 @@ class AsyncBatchEmbeddings:
|
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
@observe(_LOGGER)
|
|
158
|
-
@backoff_async(
|
|
158
|
+
@backoff_async(exceptions=[RateLimitError, InternalServerError], scale=1, max_retries=12)
|
|
159
159
|
async def _embed_chunk(self, inputs: List[str]) -> List[NDArray[np.float32]]:
|
|
160
160
|
"""Embed one minibatch of strings asynchronously.
|
|
161
161
|
|
|
@@ -2,7 +2,7 @@ from dataclasses import dataclass, field
|
|
|
2
2
|
from logging import Logger, getLogger
|
|
3
3
|
from typing import Generic, List, Type, cast
|
|
4
4
|
|
|
5
|
-
from openai import AsyncOpenAI, OpenAI, RateLimitError
|
|
5
|
+
from openai import AsyncOpenAI, InternalServerError, OpenAI, RateLimitError
|
|
6
6
|
from openai.types.responses import ParsedResponse
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
@@ -206,7 +206,7 @@ class BatchResponses(Generic[ResponseFormat]):
|
|
|
206
206
|
)
|
|
207
207
|
|
|
208
208
|
@observe(_LOGGER)
|
|
209
|
-
@backoff(
|
|
209
|
+
@backoff(exceptions=[RateLimitError, InternalServerError], scale=1, max_retries=12)
|
|
210
210
|
def _request_llm(self, user_messages: List[Message[str]]) -> ParsedResponse[Response[ResponseFormat]]:
|
|
211
211
|
"""Make a single call to the OpenAI JSON‑mode endpoint.
|
|
212
212
|
|
|
@@ -400,7 +400,7 @@ class AsyncBatchResponses(Generic[ResponseFormat]):
|
|
|
400
400
|
)
|
|
401
401
|
|
|
402
402
|
@observe(_LOGGER)
|
|
403
|
-
@backoff_async(
|
|
403
|
+
@backoff_async(exceptions=[RateLimitError, InternalServerError], scale=1, max_retries=12)
|
|
404
404
|
async def _request_llm(self, user_messages: List[Message[str]]) -> ParsedResponse[Response[ResponseFormat]]:
|
|
405
405
|
"""Make a single async call to the OpenAI JSON‑mode endpoint.
|
|
406
406
|
|
|
@@ -3,7 +3,7 @@ import functools
|
|
|
3
3
|
import re
|
|
4
4
|
import time
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Awaitable, Callable, List, TypeVar
|
|
6
|
+
from typing import Awaitable, Callable, List, Type, TypeVar
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import tiktoken
|
|
@@ -34,24 +34,28 @@ def get_exponential_with_cutoff(scale: float) -> float:
|
|
|
34
34
|
return v
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def backoff(
|
|
37
|
+
def backoff(
|
|
38
|
+
exceptions: List[Type[Exception]],
|
|
39
|
+
scale: int | None = None,
|
|
40
|
+
max_retries: int | None = None,
|
|
41
|
+
) -> Callable[..., V]:
|
|
38
42
|
"""Decorator implementing exponential back‑off retry logic.
|
|
39
43
|
|
|
40
44
|
Args:
|
|
41
|
-
|
|
45
|
+
exceptions (List[Type[Exception]]): List of exception types that trigger a retry.
|
|
42
46
|
scale (int | None): Initial scale parameter for the exponential jitter.
|
|
43
47
|
This scale is used as the mean for the first delay's exponential
|
|
44
48
|
distribution and doubles with each subsequent retry. If ``None``,
|
|
45
49
|
an initial scale of 1.0 is used.
|
|
46
|
-
max_retries (
|
|
50
|
+
max_retries (int | None): Maximum number of retries. ``None`` means
|
|
47
51
|
retry indefinitely.
|
|
48
52
|
|
|
49
53
|
Returns:
|
|
50
54
|
Callable[..., V]: A decorated function that retries on the specified
|
|
51
|
-
|
|
55
|
+
exceptions with exponential back‑off.
|
|
52
56
|
|
|
53
57
|
Raises:
|
|
54
|
-
|
|
58
|
+
Exception: Re‑raised when the maximum number of retries is exceeded.
|
|
55
59
|
"""
|
|
56
60
|
|
|
57
61
|
def decorator(func: Callable[..., V]) -> Callable[..., V]:
|
|
@@ -65,7 +69,7 @@ def backoff(exception: type[Exception], scale: int | None = None, max_retries: i
|
|
|
65
69
|
while True:
|
|
66
70
|
try:
|
|
67
71
|
return func(*args, **kwargs)
|
|
68
|
-
except
|
|
72
|
+
except tuple(exceptions):
|
|
69
73
|
attempt += 1
|
|
70
74
|
if max_retries is not None and attempt >= max_retries:
|
|
71
75
|
raise
|
|
@@ -83,12 +87,14 @@ def backoff(exception: type[Exception], scale: int | None = None, max_retries: i
|
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
def backoff_async(
|
|
86
|
-
|
|
90
|
+
exceptions: List[Type[Exception]],
|
|
91
|
+
scale: int | None = None,
|
|
92
|
+
max_retries: int | None = None,
|
|
87
93
|
) -> Callable[..., Awaitable[V]]:
|
|
88
94
|
"""Asynchronous version of the backoff decorator.
|
|
89
95
|
|
|
90
96
|
Args:
|
|
91
|
-
|
|
97
|
+
exceptions (List[Type[Exception]]): List of exception types that trigger a retry.
|
|
92
98
|
scale (int | None): Initial scale parameter for the exponential jitter.
|
|
93
99
|
This scale is used as the mean for the first delay's exponential
|
|
94
100
|
distribution and doubles with each subsequent retry. If ``None``,
|
|
@@ -98,10 +104,10 @@ def backoff_async(
|
|
|
98
104
|
|
|
99
105
|
Returns:
|
|
100
106
|
Callable[..., Awaitable[V]]: A decorated asynchronous function that
|
|
101
|
-
retries on the specified
|
|
107
|
+
retries on the specified exceptions with exponential back‑off.
|
|
102
108
|
|
|
103
109
|
Raises:
|
|
104
|
-
|
|
110
|
+
Exception: Re‑raised when the maximum number of retries is exceeded.
|
|
105
111
|
"""
|
|
106
112
|
|
|
107
113
|
def decorator(func: Callable[..., Awaitable[V]]) -> Callable[..., Awaitable[V]]:
|
|
@@ -115,7 +121,7 @@ def backoff_async(
|
|
|
115
121
|
while True:
|
|
116
122
|
try:
|
|
117
123
|
return await func(*args, **kwargs)
|
|
118
|
-
except
|
|
124
|
+
except tuple(exceptions):
|
|
119
125
|
attempt += 1
|
|
120
126
|
if max_retries is not None and attempt >= max_retries:
|
|
121
127
|
raise
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
|
|
5
|
+
import tiktoken
|
|
6
|
+
|
|
7
|
+
from openaivec.util import TextChunker, backoff, backoff_async
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestTextChunker(TestCase):
|
|
11
|
+
def setUp(self):
|
|
12
|
+
self.sep = TextChunker(
|
|
13
|
+
enc=tiktoken.encoding_for_model("text-embedding-3-large"),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def test_split(self):
|
|
17
|
+
text = """
|
|
18
|
+
Kubernetes was announced by Google on June 6, 2014.[10] The project was conceived and created by Google employees Joe Beda, Brendan Burns, and Craig McLuckie. Others at Google soon joined to help build the project including Ville Aikas, Dawn Chen, Brian Grant, Tim Hockin, and Daniel Smith.[11][12] Other companies such as Red Hat and CoreOS joined the effort soon after, with notable contributors such as Clayton Coleman and Kelsey Hightower.[10]
|
|
19
|
+
|
|
20
|
+
The design and development of Kubernetes was inspired by Google's Borg cluster manager and based on Promise Theory.[13][14] Many of its top contributors had previously worked on Borg;[15][16] they codenamed Kubernetes "Project 7" after the Star Trek ex-Borg character Seven of Nine[17] and gave its logo a seven-spoked ship's wheel (designed by Tim Hockin). Unlike Borg, which was written in C++,[15] Kubernetes is written in the Go language.
|
|
21
|
+
|
|
22
|
+
Kubernetes was announced in June, 2014 and version 1.0 was released on July 21, 2015.[18] Google worked with the Linux Foundation to form the Cloud Native Computing Foundation (CNCF)[19] and offered Kubernetes as the seed technology.
|
|
23
|
+
|
|
24
|
+
Google was already offering a managed Kubernetes service, GKE, and Red Hat was supporting Kubernetes as part of OpenShift since the inception of the Kubernetes project in 2014.[20] In 2017, the principal competitors rallied around Kubernetes and announced adding native support for it:
|
|
25
|
+
|
|
26
|
+
VMware (proponent of Pivotal Cloud Foundry)[21] in August,
|
|
27
|
+
Mesosphere, Inc. (proponent of Marathon and Mesos)[22] in September,
|
|
28
|
+
Docker, Inc. (proponent of Docker)[23] in October,
|
|
29
|
+
Microsoft Azure[24] also in October,
|
|
30
|
+
AWS announced support for Kubernetes via the Elastic Kubernetes Service (EKS)[25] in November.
|
|
31
|
+
Cisco Elastic Kubernetes Service (EKS)[26] in November.
|
|
32
|
+
On March 6, 2018, Kubernetes Project reached ninth place in the list of GitHub projects by the number of commits, and second place in authors and issues, after the Linux kernel.[27]
|
|
33
|
+
|
|
34
|
+
Until version 1.18, Kubernetes followed an N-2 support policy, meaning that the three most recent minor versions receive security updates and bug fixes.[28] Starting with version 1.19, Kubernetes follows an N-3 support policy.[29]
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
chunks = self.sep.split(text, max_tokens=256, sep=[".", "\n\n"])
|
|
38
|
+
|
|
39
|
+
# Assert that the number of chunks is as expected
|
|
40
|
+
enc = tiktoken.encoding_for_model("text-embedding-3-large")
|
|
41
|
+
|
|
42
|
+
for chunk in chunks:
|
|
43
|
+
self.assertLessEqual(len(enc.encode(chunk)), 256)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestBackoff(TestCase):
|
|
47
|
+
def test_backoff_no_exception(self):
|
|
48
|
+
"""Test that function executes normally when no exception is raised."""
|
|
49
|
+
call_count = 0
|
|
50
|
+
|
|
51
|
+
@backoff(exceptions=[ValueError], scale=0.1, max_retries=3)
|
|
52
|
+
def success_func():
|
|
53
|
+
nonlocal call_count
|
|
54
|
+
call_count += 1
|
|
55
|
+
return "success"
|
|
56
|
+
|
|
57
|
+
result = success_func()
|
|
58
|
+
self.assertEqual(result, "success")
|
|
59
|
+
self.assertEqual(call_count, 1)
|
|
60
|
+
|
|
61
|
+
def test_backoff_retries_on_exception(self):
|
|
62
|
+
"""Test that function retries on specified exception."""
|
|
63
|
+
call_count = 0
|
|
64
|
+
|
|
65
|
+
@backoff(exceptions=[ValueError], scale=0.01, max_retries=3)
|
|
66
|
+
def fail_twice():
|
|
67
|
+
nonlocal call_count
|
|
68
|
+
call_count += 1
|
|
69
|
+
if call_count < 3:
|
|
70
|
+
raise ValueError("Test error")
|
|
71
|
+
return "success"
|
|
72
|
+
|
|
73
|
+
result = fail_twice()
|
|
74
|
+
self.assertEqual(result, "success")
|
|
75
|
+
self.assertEqual(call_count, 3)
|
|
76
|
+
|
|
77
|
+
def test_backoff_multiple_exceptions(self):
|
|
78
|
+
"""Test that function retries on multiple exception types."""
|
|
79
|
+
call_count = 0
|
|
80
|
+
|
|
81
|
+
@backoff(exceptions=[ValueError, TypeError], scale=0.01, max_retries=5)
|
|
82
|
+
def fail_with_different_errors():
|
|
83
|
+
nonlocal call_count
|
|
84
|
+
call_count += 1
|
|
85
|
+
if call_count == 1:
|
|
86
|
+
raise ValueError("First error")
|
|
87
|
+
elif call_count == 2:
|
|
88
|
+
raise TypeError("Second error")
|
|
89
|
+
return "success"
|
|
90
|
+
|
|
91
|
+
result = fail_with_different_errors()
|
|
92
|
+
self.assertEqual(result, "success")
|
|
93
|
+
self.assertEqual(call_count, 3)
|
|
94
|
+
|
|
95
|
+
def test_backoff_max_retries_exceeded(self):
|
|
96
|
+
"""Test that function raises exception when max retries exceeded."""
|
|
97
|
+
call_count = 0
|
|
98
|
+
|
|
99
|
+
@backoff(exceptions=[ValueError], scale=0.01, max_retries=2)
|
|
100
|
+
def always_fail():
|
|
101
|
+
nonlocal call_count
|
|
102
|
+
call_count += 1
|
|
103
|
+
raise ValueError("Always fails")
|
|
104
|
+
|
|
105
|
+
with self.assertRaises(ValueError) as cm:
|
|
106
|
+
always_fail()
|
|
107
|
+
self.assertEqual(str(cm.exception), "Always fails")
|
|
108
|
+
self.assertEqual(call_count, 2) # Initial call + 1 retry
|
|
109
|
+
|
|
110
|
+
def test_backoff_unhandled_exception_not_retried(self):
|
|
111
|
+
"""Test that unhandled exceptions are not retried."""
|
|
112
|
+
call_count = 0
|
|
113
|
+
|
|
114
|
+
@backoff(exceptions=[ValueError], scale=0.01, max_retries=3)
|
|
115
|
+
def raise_unhandled():
|
|
116
|
+
nonlocal call_count
|
|
117
|
+
call_count += 1
|
|
118
|
+
raise TypeError("Unhandled exception")
|
|
119
|
+
|
|
120
|
+
with self.assertRaises(TypeError) as cm:
|
|
121
|
+
raise_unhandled()
|
|
122
|
+
self.assertEqual(str(cm.exception), "Unhandled exception")
|
|
123
|
+
self.assertEqual(call_count, 1) # No retries for unhandled exception
|
|
124
|
+
|
|
125
|
+
def test_backoff_exponential_delay(self):
|
|
126
|
+
"""Test that delay increases exponentially."""
|
|
127
|
+
call_times = []
|
|
128
|
+
|
|
129
|
+
@backoff(exceptions=[ValueError], scale=0.01, max_retries=3)
|
|
130
|
+
def track_timing():
|
|
131
|
+
call_times.append(time.time())
|
|
132
|
+
if len(call_times) < 3:
|
|
133
|
+
raise ValueError("Retry")
|
|
134
|
+
return "success"
|
|
135
|
+
|
|
136
|
+
result = track_timing()
|
|
137
|
+
self.assertEqual(result, "success")
|
|
138
|
+
self.assertEqual(len(call_times), 3)
|
|
139
|
+
|
|
140
|
+
# Check that delays are present (but keep them small for test speed)
|
|
141
|
+
for i in range(1, len(call_times)):
|
|
142
|
+
delay = call_times[i] - call_times[i - 1]
|
|
143
|
+
self.assertGreater(delay, 0) # Some delay exists
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TestBackoffAsync(TestCase):
|
|
147
|
+
def test_backoff_async_no_exception(self):
|
|
148
|
+
"""Test that async function executes normally when no exception is raised."""
|
|
149
|
+
call_count = 0
|
|
150
|
+
|
|
151
|
+
@backoff_async(exceptions=[ValueError], scale=0.1, max_retries=3)
|
|
152
|
+
async def success_func():
|
|
153
|
+
nonlocal call_count
|
|
154
|
+
call_count += 1
|
|
155
|
+
await asyncio.sleep(0.01)
|
|
156
|
+
return "success"
|
|
157
|
+
|
|
158
|
+
result = asyncio.run(success_func())
|
|
159
|
+
self.assertEqual(result, "success")
|
|
160
|
+
self.assertEqual(call_count, 1)
|
|
161
|
+
|
|
162
|
+
def test_backoff_async_retries_on_exception(self):
|
|
163
|
+
"""Test that async function retries on specified exception."""
|
|
164
|
+
call_count = 0
|
|
165
|
+
|
|
166
|
+
@backoff_async(exceptions=[ValueError], scale=0.01, max_retries=3)
|
|
167
|
+
async def fail_twice():
|
|
168
|
+
nonlocal call_count
|
|
169
|
+
call_count += 1
|
|
170
|
+
await asyncio.sleep(0.01)
|
|
171
|
+
if call_count < 3:
|
|
172
|
+
raise ValueError("Test error")
|
|
173
|
+
return "success"
|
|
174
|
+
|
|
175
|
+
result = asyncio.run(fail_twice())
|
|
176
|
+
self.assertEqual(result, "success")
|
|
177
|
+
self.assertEqual(call_count, 3)
|
|
178
|
+
|
|
179
|
+
def test_backoff_async_multiple_exceptions(self):
|
|
180
|
+
"""Test that async function retries on multiple exception types."""
|
|
181
|
+
call_count = 0
|
|
182
|
+
|
|
183
|
+
@backoff_async(exceptions=[ValueError, TypeError], scale=0.01, max_retries=5)
|
|
184
|
+
async def fail_with_different_errors():
|
|
185
|
+
nonlocal call_count
|
|
186
|
+
call_count += 1
|
|
187
|
+
await asyncio.sleep(0.01)
|
|
188
|
+
if call_count == 1:
|
|
189
|
+
raise ValueError("First error")
|
|
190
|
+
elif call_count == 2:
|
|
191
|
+
raise TypeError("Second error")
|
|
192
|
+
return "success"
|
|
193
|
+
|
|
194
|
+
result = asyncio.run(fail_with_different_errors())
|
|
195
|
+
self.assertEqual(result, "success")
|
|
196
|
+
self.assertEqual(call_count, 3)
|
|
197
|
+
|
|
198
|
+
def test_backoff_async_max_retries_exceeded(self):
|
|
199
|
+
"""Test that async function raises exception when max retries exceeded."""
|
|
200
|
+
call_count = 0
|
|
201
|
+
|
|
202
|
+
@backoff_async(exceptions=[ValueError], scale=0.01, max_retries=2)
|
|
203
|
+
async def always_fail():
|
|
204
|
+
nonlocal call_count
|
|
205
|
+
call_count += 1
|
|
206
|
+
await asyncio.sleep(0.01)
|
|
207
|
+
raise ValueError("Always fails")
|
|
208
|
+
|
|
209
|
+
with self.assertRaises(ValueError) as cm:
|
|
210
|
+
asyncio.run(always_fail())
|
|
211
|
+
self.assertEqual(str(cm.exception), "Always fails")
|
|
212
|
+
self.assertEqual(call_count, 2) # Initial call + 1 retry
|
|
213
|
+
|
|
214
|
+
def test_backoff_async_unhandled_exception_not_retried(self):
|
|
215
|
+
"""Test that unhandled exceptions are not retried in async."""
|
|
216
|
+
call_count = 0
|
|
217
|
+
|
|
218
|
+
@backoff_async(exceptions=[ValueError], scale=0.01, max_retries=3)
|
|
219
|
+
async def raise_unhandled():
|
|
220
|
+
nonlocal call_count
|
|
221
|
+
call_count += 1
|
|
222
|
+
await asyncio.sleep(0.01)
|
|
223
|
+
raise TypeError("Unhandled exception")
|
|
224
|
+
|
|
225
|
+
with self.assertRaises(TypeError) as cm:
|
|
226
|
+
asyncio.run(raise_unhandled())
|
|
227
|
+
self.assertEqual(str(cm.exception), "Unhandled exception")
|
|
228
|
+
self.assertEqual(call_count, 1) # No retries for unhandled exception
|
|
229
|
+
|
|
230
|
+
def test_backoff_async_with_openai_exceptions(self):
|
|
231
|
+
"""Test backoff with OpenAI exception types."""
|
|
232
|
+
# Import OpenAI exceptions for testing
|
|
233
|
+
try:
|
|
234
|
+
from openai import RateLimitError, InternalServerError
|
|
235
|
+
from unittest.mock import Mock
|
|
236
|
+
|
|
237
|
+
call_count = 0
|
|
238
|
+
|
|
239
|
+
# Create a mock response object
|
|
240
|
+
mock_response = Mock()
|
|
241
|
+
mock_response.request = Mock()
|
|
242
|
+
mock_response.status_code = 429 # For RateLimitError
|
|
243
|
+
|
|
244
|
+
@backoff_async(exceptions=[RateLimitError, InternalServerError], scale=0.01, max_retries=3)
|
|
245
|
+
async def simulate_api_errors():
|
|
246
|
+
nonlocal call_count
|
|
247
|
+
call_count += 1
|
|
248
|
+
await asyncio.sleep(0.01)
|
|
249
|
+
if call_count == 1:
|
|
250
|
+
mock_response.status_code = 429
|
|
251
|
+
raise RateLimitError("Rate limit hit", response=mock_response, body=None)
|
|
252
|
+
elif call_count == 2:
|
|
253
|
+
mock_response.status_code = 500
|
|
254
|
+
raise InternalServerError("Server error", response=mock_response, body=None)
|
|
255
|
+
return "success"
|
|
256
|
+
|
|
257
|
+
result = asyncio.run(simulate_api_errors())
|
|
258
|
+
self.assertEqual(result, "success")
|
|
259
|
+
self.assertEqual(call_count, 3)
|
|
260
|
+
except ImportError:
|
|
261
|
+
# Skip test if OpenAI is not installed
|
|
262
|
+
self.skipTest("OpenAI not installed")
|
|
263
|
+
|
|
264
|
+
def test_backoff_production_settings(self):
|
|
265
|
+
"""Test backoff with production-like settings for OpenAI API."""
|
|
266
|
+
call_count = 0
|
|
267
|
+
call_times = []
|
|
268
|
+
|
|
269
|
+
@backoff(exceptions=[ValueError], scale=1, max_retries=12)
|
|
270
|
+
def simulate_rate_limit_scenario():
|
|
271
|
+
nonlocal call_count
|
|
272
|
+
call_count += 1
|
|
273
|
+
call_times.append(time.time())
|
|
274
|
+
|
|
275
|
+
# Simulate rate limit that clears after a few retries
|
|
276
|
+
if call_count < 4:
|
|
277
|
+
raise ValueError("Rate limit hit")
|
|
278
|
+
return "success"
|
|
279
|
+
|
|
280
|
+
start_time = time.time()
|
|
281
|
+
result = simulate_rate_limit_scenario()
|
|
282
|
+
total_time = time.time() - start_time
|
|
283
|
+
|
|
284
|
+
self.assertEqual(result, "success")
|
|
285
|
+
self.assertEqual(call_count, 4)
|
|
286
|
+
self.assertEqual(len(call_times), 4)
|
|
287
|
+
|
|
288
|
+
# With scale=1, total time should be reasonable (under 10 seconds)
|
|
289
|
+
# First attempt: immediate, then ~1s, ~2s, ~4s delays
|
|
290
|
+
self.assertLess(total_time, 10)
|
|
291
|
+
self.assertGreater(total_time, 0.5) # Should have some delay
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from unittest import TestCase
|
|
2
|
-
|
|
3
|
-
import tiktoken
|
|
4
|
-
|
|
5
|
-
from openaivec.util import TextChunker
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TestTextChunker(TestCase):
|
|
9
|
-
def setUp(self):
|
|
10
|
-
self.sep = TextChunker(
|
|
11
|
-
enc=tiktoken.encoding_for_model("text-embedding-3-large"),
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
def test_split(self):
|
|
15
|
-
text = """
|
|
16
|
-
Kubernetes was announced by Google on June 6, 2014.[10] The project was conceived and created by Google employees Joe Beda, Brendan Burns, and Craig McLuckie. Others at Google soon joined to help build the project including Ville Aikas, Dawn Chen, Brian Grant, Tim Hockin, and Daniel Smith.[11][12] Other companies such as Red Hat and CoreOS joined the effort soon after, with notable contributors such as Clayton Coleman and Kelsey Hightower.[10]
|
|
17
|
-
|
|
18
|
-
The design and development of Kubernetes was inspired by Google's Borg cluster manager and based on Promise Theory.[13][14] Many of its top contributors had previously worked on Borg;[15][16] they codenamed Kubernetes "Project 7" after the Star Trek ex-Borg character Seven of Nine[17] and gave its logo a seven-spoked ship's wheel (designed by Tim Hockin). Unlike Borg, which was written in C++,[15] Kubernetes is written in the Go language.
|
|
19
|
-
|
|
20
|
-
Kubernetes was announced in June, 2014 and version 1.0 was released on July 21, 2015.[18] Google worked with the Linux Foundation to form the Cloud Native Computing Foundation (CNCF)[19] and offered Kubernetes as the seed technology.
|
|
21
|
-
|
|
22
|
-
Google was already offering a managed Kubernetes service, GKE, and Red Hat was supporting Kubernetes as part of OpenShift since the inception of the Kubernetes project in 2014.[20] In 2017, the principal competitors rallied around Kubernetes and announced adding native support for it:
|
|
23
|
-
|
|
24
|
-
VMware (proponent of Pivotal Cloud Foundry)[21] in August,
|
|
25
|
-
Mesosphere, Inc. (proponent of Marathon and Mesos)[22] in September,
|
|
26
|
-
Docker, Inc. (proponent of Docker)[23] in October,
|
|
27
|
-
Microsoft Azure[24] also in October,
|
|
28
|
-
AWS announced support for Kubernetes via the Elastic Kubernetes Service (EKS)[25] in November.
|
|
29
|
-
Cisco Elastic Kubernetes Service (EKS)[26] in November.
|
|
30
|
-
On March 6, 2018, Kubernetes Project reached ninth place in the list of GitHub projects by the number of commits, and second place in authors and issues, after the Linux kernel.[27]
|
|
31
|
-
|
|
32
|
-
Until version 1.18, Kubernetes followed an N-2 support policy, meaning that the three most recent minor versions receive security updates and bug fixes.[28] Starting with version 1.19, Kubernetes follows an N-3 support policy.[29]
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
chunks = self.sep.split(text, max_tokens=256, sep=[".", "\n\n"])
|
|
36
|
-
|
|
37
|
-
# Assert that the number of chunks is as expected
|
|
38
|
-
enc = tiktoken.encoding_for_model("text-embedding-3-large")
|
|
39
|
-
|
|
40
|
-
for chunk in chunks:
|
|
41
|
-
self.assertLessEqual(len(enc.encode(chunk)), 256)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/customer_support/inquiry_classification.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openaivec-0.13.0 → openaivec-0.13.1}/docs/api/tasks/customer_support/response_suggestion.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/customer_sentiment.py
RENAMED
|
File without changes
|
{openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/inquiry_classification.py
RENAMED
|
File without changes
|
{openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/inquiry_summary.py
RENAMED
|
File without changes
|
{openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/intent_analysis.py
RENAMED
|
File without changes
|
{openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/response_suggestion.py
RENAMED
|
File without changes
|
{openaivec-0.13.0 → openaivec-0.13.1}/src/openaivec/task/customer_support/urgency_analysis.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|