pixeltable 0.3.1__py3-none-any.whl → 0.3.3__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.
Potentially problematic release.
This version of pixeltable might be problematic. Click here for more details.
- pixeltable/__init__.py +64 -11
- pixeltable/__version__.py +2 -2
- pixeltable/catalog/__init__.py +1 -1
- pixeltable/catalog/catalog.py +50 -27
- pixeltable/catalog/column.py +27 -11
- pixeltable/catalog/dir.py +6 -4
- pixeltable/catalog/globals.py +8 -1
- pixeltable/catalog/insertable_table.py +25 -15
- pixeltable/catalog/named_function.py +10 -6
- pixeltable/catalog/path.py +3 -2
- pixeltable/catalog/path_dict.py +8 -6
- pixeltable/catalog/schema_object.py +2 -1
- pixeltable/catalog/table.py +123 -103
- pixeltable/catalog/table_version.py +292 -143
- pixeltable/catalog/table_version_path.py +8 -5
- pixeltable/catalog/view.py +68 -27
- pixeltable/dataframe.py +102 -72
- pixeltable/env.py +39 -23
- pixeltable/exec/__init__.py +2 -2
- pixeltable/exec/aggregation_node.py +10 -4
- pixeltable/exec/cache_prefetch_node.py +5 -3
- pixeltable/exec/component_iteration_node.py +9 -8
- pixeltable/exec/data_row_batch.py +21 -10
- pixeltable/exec/exec_context.py +10 -3
- pixeltable/exec/exec_node.py +23 -12
- pixeltable/exec/expr_eval/evaluators.py +18 -17
- pixeltable/exec/expr_eval/expr_eval_node.py +29 -16
- pixeltable/exec/expr_eval/globals.py +33 -11
- pixeltable/exec/expr_eval/row_buffer.py +5 -6
- pixeltable/exec/expr_eval/schedulers.py +170 -42
- pixeltable/exec/in_memory_data_node.py +8 -7
- pixeltable/exec/row_update_node.py +15 -5
- pixeltable/exec/sql_node.py +56 -27
- pixeltable/exprs/__init__.py +2 -2
- pixeltable/exprs/arithmetic_expr.py +57 -26
- pixeltable/exprs/array_slice.py +1 -1
- pixeltable/exprs/column_property_ref.py +2 -1
- pixeltable/exprs/column_ref.py +20 -15
- pixeltable/exprs/comparison.py +6 -2
- pixeltable/exprs/compound_predicate.py +1 -3
- pixeltable/exprs/data_row.py +2 -2
- pixeltable/exprs/expr.py +101 -72
- pixeltable/exprs/expr_dict.py +2 -1
- pixeltable/exprs/expr_set.py +3 -1
- pixeltable/exprs/function_call.py +39 -41
- pixeltable/exprs/globals.py +1 -0
- pixeltable/exprs/in_predicate.py +2 -2
- pixeltable/exprs/inline_expr.py +20 -17
- pixeltable/exprs/json_mapper.py +4 -2
- pixeltable/exprs/json_path.py +12 -18
- pixeltable/exprs/literal.py +5 -9
- pixeltable/exprs/method_ref.py +1 -0
- pixeltable/exprs/object_ref.py +1 -1
- pixeltable/exprs/row_builder.py +31 -16
- pixeltable/exprs/rowid_ref.py +14 -5
- pixeltable/exprs/similarity_expr.py +11 -6
- pixeltable/exprs/sql_element_cache.py +1 -1
- pixeltable/exprs/type_cast.py +24 -9
- pixeltable/ext/__init__.py +1 -0
- pixeltable/ext/functions/__init__.py +1 -0
- pixeltable/ext/functions/whisperx.py +2 -2
- pixeltable/ext/functions/yolox.py +11 -11
- pixeltable/func/aggregate_function.py +17 -13
- pixeltable/func/callable_function.py +6 -6
- pixeltable/func/expr_template_function.py +15 -14
- pixeltable/func/function.py +16 -16
- pixeltable/func/function_registry.py +11 -8
- pixeltable/func/globals.py +4 -2
- pixeltable/func/query_template_function.py +12 -13
- pixeltable/func/signature.py +18 -9
- pixeltable/func/tools.py +10 -17
- pixeltable/func/udf.py +106 -11
- pixeltable/functions/__init__.py +21 -2
- pixeltable/functions/anthropic.py +21 -15
- pixeltable/functions/fireworks.py +63 -5
- pixeltable/functions/gemini.py +13 -3
- pixeltable/functions/globals.py +18 -6
- pixeltable/functions/huggingface.py +20 -38
- pixeltable/functions/image.py +7 -3
- pixeltable/functions/json.py +1 -0
- pixeltable/functions/llama_cpp.py +1 -4
- pixeltable/functions/mistralai.py +31 -20
- pixeltable/functions/ollama.py +4 -18
- pixeltable/functions/openai.py +214 -109
- pixeltable/functions/replicate.py +11 -10
- pixeltable/functions/string.py +70 -7
- pixeltable/functions/timestamp.py +21 -8
- pixeltable/functions/together.py +66 -52
- pixeltable/functions/video.py +1 -0
- pixeltable/functions/vision.py +14 -11
- pixeltable/functions/whisper.py +2 -1
- pixeltable/globals.py +61 -28
- pixeltable/index/__init__.py +1 -1
- pixeltable/index/btree.py +5 -3
- pixeltable/index/embedding_index.py +15 -14
- pixeltable/io/__init__.py +1 -1
- pixeltable/io/external_store.py +30 -25
- pixeltable/io/fiftyone.py +6 -14
- pixeltable/io/globals.py +33 -27
- pixeltable/io/hf_datasets.py +3 -2
- pixeltable/io/label_studio.py +80 -71
- pixeltable/io/pandas.py +33 -9
- pixeltable/io/parquet.py +10 -13
- pixeltable/iterators/__init__.py +1 -0
- pixeltable/iterators/audio.py +205 -0
- pixeltable/iterators/document.py +19 -8
- pixeltable/iterators/image.py +6 -24
- pixeltable/iterators/string.py +3 -6
- pixeltable/iterators/video.py +1 -7
- pixeltable/metadata/__init__.py +9 -2
- pixeltable/metadata/converters/convert_10.py +2 -2
- pixeltable/metadata/converters/convert_15.py +1 -5
- pixeltable/metadata/converters/convert_16.py +2 -4
- pixeltable/metadata/converters/convert_17.py +2 -4
- pixeltable/metadata/converters/convert_18.py +2 -4
- pixeltable/metadata/converters/convert_19.py +2 -5
- pixeltable/metadata/converters/convert_20.py +1 -4
- pixeltable/metadata/converters/convert_21.py +4 -6
- pixeltable/metadata/converters/convert_22.py +1 -0
- pixeltable/metadata/converters/convert_23.py +5 -5
- pixeltable/metadata/converters/convert_24.py +12 -13
- pixeltable/metadata/converters/convert_26.py +23 -0
- pixeltable/metadata/converters/util.py +3 -4
- pixeltable/metadata/notes.py +1 -0
- pixeltable/metadata/schema.py +13 -2
- pixeltable/plan.py +173 -98
- pixeltable/store.py +42 -26
- pixeltable/type_system.py +130 -85
- pixeltable/utils/arrow.py +1 -7
- pixeltable/utils/coco.py +16 -17
- pixeltable/utils/code.py +1 -1
- pixeltable/utils/console_output.py +44 -0
- pixeltable/utils/description_helper.py +7 -7
- pixeltable/utils/documents.py +3 -1
- pixeltable/utils/filecache.py +13 -8
- pixeltable/utils/http_server.py +9 -8
- pixeltable/utils/media_store.py +2 -1
- pixeltable/utils/pytorch.py +11 -14
- pixeltable/utils/s3.py +1 -0
- pixeltable/utils/sql.py +1 -0
- pixeltable/utils/transactional_directory.py +2 -2
- {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/METADATA +7 -8
- pixeltable-0.3.3.dist-info/RECORD +163 -0
- pixeltable-0.3.1.dist-info/RECORD +0 -160
- {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/LICENSE +0 -0
- {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/WHEEL +0 -0
- {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/entry_points.txt +0 -0
pixeltable/functions/openai.py
CHANGED
|
@@ -10,15 +10,15 @@ import datetime
|
|
|
10
10
|
import io
|
|
11
11
|
import json
|
|
12
12
|
import logging
|
|
13
|
+
import math
|
|
13
14
|
import pathlib
|
|
14
15
|
import re
|
|
15
16
|
import uuid
|
|
16
|
-
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, Union, cast
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Type, TypeVar, Union, cast
|
|
17
18
|
|
|
18
|
-
import PIL.Image
|
|
19
19
|
import httpx
|
|
20
20
|
import numpy as np
|
|
21
|
-
import
|
|
21
|
+
import PIL
|
|
22
22
|
|
|
23
23
|
import pixeltable as pxt
|
|
24
24
|
from pixeltable import env, exprs
|
|
@@ -32,36 +32,18 @@ _logger = logging.getLogger('pixeltable')
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@env.register_client('openai')
|
|
35
|
-
def _(api_key: str) ->
|
|
35
|
+
def _(api_key: str) -> 'openai.AsyncOpenAI':
|
|
36
36
|
import openai
|
|
37
|
-
return (
|
|
38
|
-
openai.OpenAI(api_key=api_key),
|
|
39
|
-
openai.AsyncOpenAI(
|
|
40
|
-
api_key=api_key,
|
|
41
|
-
# recommended to increase limits for async client to avoid connection errors
|
|
42
|
-
http_client=httpx.AsyncClient(limits=httpx.Limits(max_keepalive_connections=100, max_connections=500)),
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _openai_client() -> 'openai.OpenAI':
|
|
48
|
-
return env.Env.get().get_client('openai')[0]
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
38
|
+
return openai.AsyncOpenAI(
|
|
39
|
+
api_key=api_key,
|
|
40
|
+
# recommended to increase limits for async client to avoid connection errors
|
|
41
|
+
http_client=httpx.AsyncClient(limits=httpx.Limits(max_keepalive_connections=100, max_connections=500)),
|
|
42
|
+
)
|
|
53
43
|
|
|
54
44
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# by OpenAI. Should we investigate making this more customizable in the future?
|
|
58
|
-
def _retry(fn: Callable) -> Callable:
|
|
59
|
-
import openai
|
|
60
|
-
return tenacity.retry(
|
|
61
|
-
retry=tenacity.retry_if_exception_type(openai.RateLimitError),
|
|
62
|
-
wait=tenacity.wait_random_exponential(multiplier=1, max=60),
|
|
63
|
-
stop=tenacity.stop_after_attempt(20),
|
|
64
|
-
)(fn)
|
|
45
|
+
def _openai_client() -> 'openai.AsyncOpenAI':
|
|
46
|
+
return env.Env.get().get_client('openai')
|
|
65
47
|
|
|
66
48
|
|
|
67
49
|
# models that share rate limits; see https://platform.openai.com/settings/organization/limits for details
|
|
@@ -72,7 +54,7 @@ _shared_rate_limits = {
|
|
|
72
54
|
'gpt-4-turbo-2024-04-09',
|
|
73
55
|
'gpt-4-turbo-preview',
|
|
74
56
|
'gpt-4-0125-preview',
|
|
75
|
-
'gpt-4-1106-preview'
|
|
57
|
+
'gpt-4-1106-preview',
|
|
76
58
|
],
|
|
77
59
|
'gpt-4o': [
|
|
78
60
|
'gpt-4o',
|
|
@@ -82,24 +64,24 @@ _shared_rate_limits = {
|
|
|
82
64
|
'gpt-4o-2024-11-20',
|
|
83
65
|
'gpt-4o-audio-preview',
|
|
84
66
|
'gpt-4o-audio-preview-2024-10-01',
|
|
85
|
-
'gpt-4o-audio-preview-2024-12-17'
|
|
67
|
+
'gpt-4o-audio-preview-2024-12-17',
|
|
86
68
|
],
|
|
87
69
|
'gpt-4o-mini': [
|
|
88
70
|
'gpt-4o-mini',
|
|
89
71
|
'gpt-4o-mini-latest',
|
|
90
72
|
'gpt-4o-mini-2024-07-18',
|
|
91
73
|
'gpt-4o-mini-audio-preview',
|
|
92
|
-
'gpt-4o-mini-audio-preview-2024-12-17'
|
|
74
|
+
'gpt-4o-mini-audio-preview-2024-12-17',
|
|
93
75
|
],
|
|
94
76
|
'gpt-4o-mini-realtime-preview': [
|
|
95
77
|
'gpt-4o-mini-realtime-preview',
|
|
96
78
|
'gpt-4o-mini-realtime-preview-latest',
|
|
97
|
-
'gpt-4o-mini-realtime-preview-2024-12-17'
|
|
98
|
-
]
|
|
79
|
+
'gpt-4o-mini-realtime-preview-2024-12-17',
|
|
80
|
+
],
|
|
99
81
|
}
|
|
100
82
|
|
|
101
83
|
|
|
102
|
-
def
|
|
84
|
+
def _rate_limits_pool(model: str) -> str:
|
|
103
85
|
for model_family, models in _shared_rate_limits.items():
|
|
104
86
|
if model in models:
|
|
105
87
|
return f'rate-limits:openai:{model_family}'
|
|
@@ -112,8 +94,18 @@ class OpenAIRateLimitsInfo(env.RateLimitsInfo):
|
|
|
112
94
|
def __init__(self, get_request_resources: Callable[..., dict[str, int]]):
|
|
113
95
|
super().__init__(get_request_resources)
|
|
114
96
|
import openai
|
|
97
|
+
|
|
115
98
|
self.retryable_errors = (
|
|
116
|
-
|
|
99
|
+
# ConnectionError: we occasionally see this error when the AsyncConnectionPool is trying to close
|
|
100
|
+
# expired connections
|
|
101
|
+
# (AsyncConnectionPool._close_expired_connections() fails with ConnectionError when executing
|
|
102
|
+
# 'await connection.aclose()', which is very likely a bug in AsyncConnectionPool)
|
|
103
|
+
openai.APIConnectionError,
|
|
104
|
+
# the following errors are retryable according to OpenAI's API documentation
|
|
105
|
+
openai.RateLimitError,
|
|
106
|
+
openai.APITimeoutError,
|
|
107
|
+
openai.UnprocessableEntityError,
|
|
108
|
+
openai.InternalServerError,
|
|
117
109
|
)
|
|
118
110
|
|
|
119
111
|
def get_retry_delay(self, exc: Exception) -> Optional[float]:
|
|
@@ -133,7 +125,7 @@ _header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d
|
|
|
133
125
|
def _parse_header_duration(duration_str):
|
|
134
126
|
match = _header_duration_pattern.match(duration_str)
|
|
135
127
|
if not match:
|
|
136
|
-
raise ValueError(
|
|
128
|
+
raise ValueError('Invalid duration format')
|
|
137
129
|
|
|
138
130
|
days = int(match.group(1) or 0)
|
|
139
131
|
hours = int(match.group(2) or 0)
|
|
@@ -141,17 +133,11 @@ def _parse_header_duration(duration_str):
|
|
|
141
133
|
minutes = int(match.group(4) or 0)
|
|
142
134
|
seconds = float(match.group(5) or 0)
|
|
143
135
|
|
|
144
|
-
return datetime.timedelta(
|
|
145
|
-
days=days,
|
|
146
|
-
hours=hours,
|
|
147
|
-
minutes=minutes,
|
|
148
|
-
seconds=seconds,
|
|
149
|
-
milliseconds=milliseconds
|
|
150
|
-
)
|
|
136
|
+
return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
|
|
151
137
|
|
|
152
138
|
|
|
153
139
|
def _get_header_info(
|
|
154
|
-
|
|
140
|
+
headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
|
|
155
141
|
) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
|
|
156
142
|
assert requests or tokens
|
|
157
143
|
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
@@ -184,8 +170,14 @@ def _get_header_info(
|
|
|
184
170
|
|
|
185
171
|
|
|
186
172
|
@pxt.udf
|
|
187
|
-
def speech(
|
|
188
|
-
|
|
173
|
+
async def speech(
|
|
174
|
+
input: str,
|
|
175
|
+
*,
|
|
176
|
+
model: str,
|
|
177
|
+
voice: str,
|
|
178
|
+
response_format: Optional[str] = None,
|
|
179
|
+
speed: Optional[float] = None,
|
|
180
|
+
timeout: Optional[float] = None,
|
|
189
181
|
) -> pxt.Audio:
|
|
190
182
|
"""
|
|
191
183
|
Generates audio from the input text.
|
|
@@ -193,6 +185,10 @@ def speech(
|
|
|
193
185
|
Equivalent to the OpenAI `audio/speech` API endpoint.
|
|
194
186
|
For additional details, see: [https://platform.openai.com/docs/guides/text-to-speech](https://platform.openai.com/docs/guides/text-to-speech)
|
|
195
187
|
|
|
188
|
+
Request throttling:
|
|
189
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
190
|
+
limit is configured, uses a default of 600 RPM.
|
|
191
|
+
|
|
196
192
|
__Requirements:__
|
|
197
193
|
|
|
198
194
|
- `pip install openai`
|
|
@@ -212,10 +208,15 @@ def speech(
|
|
|
212
208
|
Add a computed column that applies the model `tts-1` to an existing Pixeltable column `tbl.text`
|
|
213
209
|
of the table `tbl`:
|
|
214
210
|
|
|
215
|
-
>>> tbl
|
|
211
|
+
>>> tbl.add_computed_column(audio=speech(tbl.text, model='tts-1', voice='nova'))
|
|
216
212
|
"""
|
|
217
|
-
content =
|
|
218
|
-
input=input,
|
|
213
|
+
content = await _openai_client().audio.speech.create(
|
|
214
|
+
input=input,
|
|
215
|
+
model=model,
|
|
216
|
+
voice=voice, # type: ignore
|
|
217
|
+
response_format=_opt(response_format), # type: ignore
|
|
218
|
+
speed=_opt(speed),
|
|
219
|
+
timeout=_opt(timeout),
|
|
219
220
|
)
|
|
220
221
|
ext = response_format or 'mp3'
|
|
221
222
|
output_filename = str(env.Env.get().tmp_dir / f'{uuid.uuid4()}.{ext}')
|
|
@@ -224,13 +225,14 @@ def speech(
|
|
|
224
225
|
|
|
225
226
|
|
|
226
227
|
@pxt.udf
|
|
227
|
-
def transcriptions(
|
|
228
|
+
async def transcriptions(
|
|
228
229
|
audio: pxt.Audio,
|
|
229
230
|
*,
|
|
230
231
|
model: str,
|
|
231
232
|
language: Optional[str] = None,
|
|
232
233
|
prompt: Optional[str] = None,
|
|
233
234
|
temperature: Optional[float] = None,
|
|
235
|
+
timeout: Optional[float] = None,
|
|
234
236
|
) -> dict:
|
|
235
237
|
"""
|
|
236
238
|
Transcribes audio into the input language.
|
|
@@ -238,6 +240,10 @@ def transcriptions(
|
|
|
238
240
|
Equivalent to the OpenAI `audio/transcriptions` API endpoint.
|
|
239
241
|
For additional details, see: [https://platform.openai.com/docs/guides/speech-to-text](https://platform.openai.com/docs/guides/speech-to-text)
|
|
240
242
|
|
|
243
|
+
Request throttling:
|
|
244
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
245
|
+
limit is configured, uses a default of 600 RPM.
|
|
246
|
+
|
|
241
247
|
__Requirements:__
|
|
242
248
|
|
|
243
249
|
- `pip install openai`
|
|
@@ -255,22 +261,28 @@ def transcriptions(
|
|
|
255
261
|
Add a computed column that applies the model `whisper-1` to an existing Pixeltable column `tbl.audio`
|
|
256
262
|
of the table `tbl`:
|
|
257
263
|
|
|
258
|
-
>>> tbl
|
|
264
|
+
>>> tbl.add_computed_column(transcription=transcriptions(tbl.audio, model='whisper-1', language='en'))
|
|
259
265
|
"""
|
|
260
266
|
file = pathlib.Path(audio)
|
|
261
|
-
transcription =
|
|
262
|
-
file=file,
|
|
267
|
+
transcription = await _openai_client().audio.transcriptions.create(
|
|
268
|
+
file=file,
|
|
269
|
+
model=model,
|
|
270
|
+
language=_opt(language),
|
|
271
|
+
prompt=_opt(prompt),
|
|
272
|
+
temperature=_opt(temperature),
|
|
273
|
+
timeout=_opt(timeout),
|
|
263
274
|
)
|
|
264
275
|
return transcription.dict()
|
|
265
276
|
|
|
266
277
|
|
|
267
278
|
@pxt.udf
|
|
268
|
-
def translations(
|
|
279
|
+
async def translations(
|
|
269
280
|
audio: pxt.Audio,
|
|
270
281
|
*,
|
|
271
282
|
model: str,
|
|
272
283
|
prompt: Optional[str] = None,
|
|
273
|
-
temperature: Optional[float] = None
|
|
284
|
+
temperature: Optional[float] = None,
|
|
285
|
+
timeout: Optional[float] = None,
|
|
274
286
|
) -> dict:
|
|
275
287
|
"""
|
|
276
288
|
Translates audio into English.
|
|
@@ -278,6 +290,10 @@ def translations(
|
|
|
278
290
|
Equivalent to the OpenAI `audio/translations` API endpoint.
|
|
279
291
|
For additional details, see: [https://platform.openai.com/docs/guides/speech-to-text](https://platform.openai.com/docs/guides/speech-to-text)
|
|
280
292
|
|
|
293
|
+
Request throttling:
|
|
294
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
295
|
+
limit is configured, uses a default of 600 RPM.
|
|
296
|
+
|
|
281
297
|
__Requirements:__
|
|
282
298
|
|
|
283
299
|
- `pip install openai`
|
|
@@ -295,11 +311,11 @@ def translations(
|
|
|
295
311
|
Add a computed column that applies the model `whisper-1` to an existing Pixeltable column `tbl.audio`
|
|
296
312
|
of the table `tbl`:
|
|
297
313
|
|
|
298
|
-
>>> tbl
|
|
314
|
+
>>> tbl.add_computed_column(translation=translations(tbl.audio, model='whisper-1', language='en'))
|
|
299
315
|
"""
|
|
300
316
|
file = pathlib.Path(audio)
|
|
301
|
-
translation =
|
|
302
|
-
file=file, model=model, prompt=_opt(prompt), temperature=_opt(temperature)
|
|
317
|
+
translation = await _openai_client().audio.translations.create(
|
|
318
|
+
file=file, model=model, prompt=_opt(prompt), temperature=_opt(temperature), timeout=_opt(timeout)
|
|
303
319
|
)
|
|
304
320
|
return translation.dict()
|
|
305
321
|
|
|
@@ -309,7 +325,7 @@ def translations(
|
|
|
309
325
|
|
|
310
326
|
|
|
311
327
|
def _chat_completions_get_request_resources(
|
|
312
|
-
|
|
328
|
+
messages: list, max_tokens: Optional[int], n: Optional[int]
|
|
313
329
|
) -> dict[str, int]:
|
|
314
330
|
completion_tokens = n * max_tokens
|
|
315
331
|
|
|
@@ -318,7 +334,7 @@ def _chat_completions_get_request_resources(
|
|
|
318
334
|
num_tokens += 4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
|
|
319
335
|
for key, value in message.items():
|
|
320
336
|
num_tokens += len(value) / 4
|
|
321
|
-
if key ==
|
|
337
|
+
if key == 'name': # if there's a name, the role is omitted
|
|
322
338
|
num_tokens -= 1 # role is always required and always 1 token
|
|
323
339
|
num_tokens += 2 # every reply is primed with <im_start>assistant
|
|
324
340
|
return {'requests': 1, 'tokens': int(num_tokens) + completion_tokens}
|
|
@@ -344,6 +360,7 @@ async def chat_completions(
|
|
|
344
360
|
tools: Optional[list[dict]] = None,
|
|
345
361
|
tool_choice: Optional[dict] = None,
|
|
346
362
|
user: Optional[str] = None,
|
|
363
|
+
timeout: Optional[float] = None,
|
|
347
364
|
) -> dict:
|
|
348
365
|
"""
|
|
349
366
|
Creates a model response for the given chat conversation.
|
|
@@ -351,6 +368,10 @@ async def chat_completions(
|
|
|
351
368
|
Equivalent to the OpenAI `chat/completions` API endpoint.
|
|
352
369
|
For additional details, see: [https://platform.openai.com/docs/guides/chat-completions](https://platform.openai.com/docs/guides/chat-completions)
|
|
353
370
|
|
|
371
|
+
Request throttling:
|
|
372
|
+
Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
|
|
373
|
+
request and token capacity. No configuration is necessary.
|
|
374
|
+
|
|
354
375
|
__Requirements:__
|
|
355
376
|
|
|
356
377
|
- `pip install openai`
|
|
@@ -372,16 +393,10 @@ async def chat_completions(
|
|
|
372
393
|
{'role': 'system', 'content': 'You are a helpful assistant.'},
|
|
373
394
|
{'role': 'user', 'content': tbl.prompt}
|
|
374
395
|
]
|
|
375
|
-
tbl
|
|
396
|
+
tbl.add_computed_column(response=chat_completions(messages, model='gpt-4o-mini'))
|
|
376
397
|
"""
|
|
377
398
|
if tools is not None:
|
|
378
|
-
tools = [
|
|
379
|
-
{
|
|
380
|
-
'type': 'function',
|
|
381
|
-
'function': tool
|
|
382
|
-
}
|
|
383
|
-
for tool in tools
|
|
384
|
-
]
|
|
399
|
+
tools = [{'type': 'function', 'function': tool} for tool in tools]
|
|
385
400
|
|
|
386
401
|
tool_choice_: Union[str, dict, None] = None
|
|
387
402
|
if tool_choice is not None:
|
|
@@ -391,17 +406,20 @@ async def chat_completions(
|
|
|
391
406
|
tool_choice_ = 'required'
|
|
392
407
|
else:
|
|
393
408
|
assert tool_choice['tool'] is not None
|
|
394
|
-
tool_choice_ = {
|
|
395
|
-
'type': 'function',
|
|
396
|
-
'function': {'name': tool_choice['tool']}
|
|
397
|
-
}
|
|
409
|
+
tool_choice_ = {'type': 'function', 'function': {'name': tool_choice['tool']}}
|
|
398
410
|
|
|
399
411
|
extra_body: Optional[dict[str, Any]] = None
|
|
400
412
|
if tool_choice is not None and not tool_choice['parallel_tool_calls']:
|
|
401
413
|
extra_body = {'parallel_tool_calls': False}
|
|
402
414
|
|
|
415
|
+
# make sure the pool info exists prior to making the request
|
|
416
|
+
resource_pool = _rate_limits_pool(model)
|
|
417
|
+
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
418
|
+
resource_pool, lambda: OpenAIRateLimitsInfo(_chat_completions_get_request_resources)
|
|
419
|
+
)
|
|
420
|
+
|
|
403
421
|
# cast(Any, ...): avoid mypy errors
|
|
404
|
-
result = await
|
|
422
|
+
result = await _openai_client().chat.completions.with_raw_response.create(
|
|
405
423
|
messages=messages,
|
|
406
424
|
model=model,
|
|
407
425
|
frequency_penalty=_opt(frequency_penalty),
|
|
@@ -419,27 +437,65 @@ async def chat_completions(
|
|
|
419
437
|
tools=_opt(cast(Any, tools)),
|
|
420
438
|
tool_choice=_opt(cast(Any, tool_choice_)),
|
|
421
439
|
user=_opt(user),
|
|
422
|
-
timeout=
|
|
440
|
+
timeout=_opt(timeout),
|
|
423
441
|
extra_body=extra_body,
|
|
424
442
|
)
|
|
425
443
|
|
|
426
|
-
resource_pool = _resource_pool(model)
|
|
427
444
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
428
|
-
rate_limits_info = env.Env.get().get_resource_pool_info(resource_pool, lambda: OpenAIRateLimitsInfo(
|
|
429
|
-
_chat_completions_get_request_resources))
|
|
430
445
|
rate_limits_info.record(requests=requests_info, tokens=tokens_info)
|
|
431
446
|
|
|
432
447
|
return json.loads(result.text)
|
|
433
448
|
|
|
434
449
|
|
|
450
|
+
def _vision_get_request_resources(
|
|
451
|
+
prompt: str, image: PIL.Image.Image, max_tokens: Optional[int], n: Optional[int]
|
|
452
|
+
) -> dict[str, int]:
|
|
453
|
+
completion_tokens = n * max_tokens
|
|
454
|
+
prompt_tokens = len(prompt) / 4
|
|
455
|
+
|
|
456
|
+
# calculate image tokens based on
|
|
457
|
+
# https://platform.openai.com/docs/guides/vision/calculating-costs#calculating-costs
|
|
458
|
+
# assuming detail='high' (which appears to be the default, according to community forum posts)
|
|
459
|
+
|
|
460
|
+
# number of 512x512 crops; ceil(): partial crops still count as full crops
|
|
461
|
+
crops_width = math.ceil(image.width / 512)
|
|
462
|
+
crops_height = math.ceil(image.height / 512)
|
|
463
|
+
total_crops = crops_width * crops_height
|
|
464
|
+
|
|
465
|
+
BASE_TOKENS = 85 # base cost for the initial 512x512 overview
|
|
466
|
+
CROP_TOKENS = 170 # cost per additional 512x512 crop
|
|
467
|
+
img_tokens = BASE_TOKENS + (CROP_TOKENS * total_crops)
|
|
468
|
+
|
|
469
|
+
total_tokens = (
|
|
470
|
+
prompt_tokens
|
|
471
|
+
+ img_tokens
|
|
472
|
+
+ completion_tokens
|
|
473
|
+
+ 4 # for <im_start>{role/name}\n{content}<im_end>\n
|
|
474
|
+
+ 2 # for reply's <im_start>assistant
|
|
475
|
+
)
|
|
476
|
+
return {'requests': 1, 'tokens': int(total_tokens)}
|
|
477
|
+
|
|
478
|
+
|
|
435
479
|
@pxt.udf
|
|
436
|
-
def vision(
|
|
480
|
+
async def vision(
|
|
481
|
+
prompt: str,
|
|
482
|
+
image: PIL.Image.Image,
|
|
483
|
+
*,
|
|
484
|
+
model: str,
|
|
485
|
+
max_tokens: Optional[int] = 1024,
|
|
486
|
+
n: Optional[int] = 1,
|
|
487
|
+
timeout: Optional[float] = None,
|
|
488
|
+
) -> str:
|
|
437
489
|
"""
|
|
438
490
|
Analyzes an image with the OpenAI vision capability. This is a convenience function that takes an image and
|
|
439
491
|
prompt, and constructs a chat completion request that utilizes OpenAI vision.
|
|
440
492
|
|
|
441
493
|
For additional details, see: [https://platform.openai.com/docs/guides/vision](https://platform.openai.com/docs/guides/vision)
|
|
442
494
|
|
|
495
|
+
Request throttling:
|
|
496
|
+
Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
|
|
497
|
+
request and token capacity. No configuration is necessary.
|
|
498
|
+
|
|
443
499
|
__Requirements:__
|
|
444
500
|
|
|
445
501
|
- `pip install openai`
|
|
@@ -456,7 +512,7 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
|
|
|
456
512
|
Add a computed column that applies the model `gpt-4o-mini` to an existing Pixeltable column `tbl.image`
|
|
457
513
|
of the table `tbl`:
|
|
458
514
|
|
|
459
|
-
>>> tbl
|
|
515
|
+
>>> tbl.add_computed_column(response=vision("What's in this image?", tbl.image, model='gpt-4o-mini'))
|
|
460
516
|
"""
|
|
461
517
|
# TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
|
|
462
518
|
bytes_arr = io.BytesIO()
|
|
@@ -472,8 +528,25 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
|
|
|
472
528
|
],
|
|
473
529
|
}
|
|
474
530
|
]
|
|
475
|
-
|
|
476
|
-
|
|
531
|
+
|
|
532
|
+
# make sure the pool info exists prior to making the request
|
|
533
|
+
resource_pool = _rate_limits_pool(model)
|
|
534
|
+
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
535
|
+
resource_pool, lambda: OpenAIRateLimitsInfo(_vision_get_request_resources)
|
|
536
|
+
)
|
|
537
|
+
result = await _openai_client().chat.completions.with_raw_response.create(
|
|
538
|
+
messages=messages, # type: ignore
|
|
539
|
+
model=model,
|
|
540
|
+
max_tokens=_opt(max_tokens),
|
|
541
|
+
n=_opt(n),
|
|
542
|
+
timeout=_opt(timeout),
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
requests_info, tokens_info = _get_header_info(result.headers)
|
|
546
|
+
rate_limits_info.record(requests=requests_info, tokens=tokens_info)
|
|
547
|
+
|
|
548
|
+
result = json.loads(result.text)
|
|
549
|
+
return result['choices'][0]['message']['content']
|
|
477
550
|
|
|
478
551
|
|
|
479
552
|
#####################################
|
|
@@ -493,7 +566,12 @@ def _embeddings_get_request_resources(input: list[str]) -> dict[str, int]:
|
|
|
493
566
|
|
|
494
567
|
@pxt.udf(batch_size=32)
|
|
495
568
|
async def embeddings(
|
|
496
|
-
input: Batch[str],
|
|
569
|
+
input: Batch[str],
|
|
570
|
+
*,
|
|
571
|
+
model: str,
|
|
572
|
+
dimensions: Optional[int] = None,
|
|
573
|
+
user: Optional[str] = None,
|
|
574
|
+
timeout: Optional[float] = None,
|
|
497
575
|
) -> Batch[pxt.Array[(None,), pxt.Float]]:
|
|
498
576
|
"""
|
|
499
577
|
Creates an embedding vector representing the input text.
|
|
@@ -501,6 +579,10 @@ async def embeddings(
|
|
|
501
579
|
Equivalent to the OpenAI `embeddings` API endpoint.
|
|
502
580
|
For additional details, see: [https://platform.openai.com/docs/guides/embeddings](https://platform.openai.com/docs/guides/embeddings)
|
|
503
581
|
|
|
582
|
+
Request throttling:
|
|
583
|
+
Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
|
|
584
|
+
request and token capacity. No configuration is necessary.
|
|
585
|
+
|
|
504
586
|
__Requirements:__
|
|
505
587
|
|
|
506
588
|
- `pip install openai`
|
|
@@ -520,16 +602,26 @@ async def embeddings(
|
|
|
520
602
|
Add a computed column that applies the model `text-embedding-3-small` to an existing
|
|
521
603
|
Pixeltable column `tbl.text` of the table `tbl`:
|
|
522
604
|
|
|
523
|
-
>>> tbl
|
|
605
|
+
>>> tbl.add_computed_column(embed=embeddings(tbl.text, model='text-embedding-3-small'))
|
|
606
|
+
|
|
607
|
+
Add an embedding index to an existing column `text`, using the model `text-embedding-3-small`:
|
|
608
|
+
|
|
609
|
+
>>> tbl.add_embedding_index(embedding=embeddings.using(model='text-embedding-3-small'))
|
|
524
610
|
"""
|
|
525
611
|
_logger.debug(f'embeddings: batch_size={len(input)}')
|
|
526
|
-
|
|
527
|
-
|
|
612
|
+
resource_pool = _rate_limits_pool(model)
|
|
613
|
+
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
614
|
+
resource_pool, lambda: OpenAIRateLimitsInfo(_embeddings_get_request_resources)
|
|
615
|
+
)
|
|
616
|
+
result = await _openai_client().embeddings.with_raw_response.create(
|
|
617
|
+
input=input,
|
|
618
|
+
model=model,
|
|
619
|
+
dimensions=_opt(dimensions),
|
|
620
|
+
user=_opt(user),
|
|
621
|
+
encoding_format='float',
|
|
622
|
+
timeout=_opt(timeout),
|
|
528
623
|
)
|
|
529
|
-
resource_pool = _resource_pool(model)
|
|
530
624
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
531
|
-
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
532
|
-
resource_pool, lambda: OpenAIRateLimitsInfo(_embeddings_get_request_resources))
|
|
533
625
|
rate_limits_info.record(requests=requests_info, tokens=tokens_info)
|
|
534
626
|
return [np.array(data['embedding'], dtype=np.float64) for data in json.loads(result.content)['data']]
|
|
535
627
|
|
|
@@ -549,7 +641,7 @@ def _(model: str, dimensions: Optional[int] = None) -> pxt.ArrayType:
|
|
|
549
641
|
|
|
550
642
|
|
|
551
643
|
@pxt.udf
|
|
552
|
-
def image_generations(
|
|
644
|
+
async def image_generations(
|
|
553
645
|
prompt: str,
|
|
554
646
|
*,
|
|
555
647
|
model: str = 'dall-e-2',
|
|
@@ -557,6 +649,7 @@ def image_generations(
|
|
|
557
649
|
size: Optional[str] = None,
|
|
558
650
|
style: Optional[str] = None,
|
|
559
651
|
user: Optional[str] = None,
|
|
652
|
+
timeout: Optional[float] = None,
|
|
560
653
|
) -> PIL.Image.Image:
|
|
561
654
|
"""
|
|
562
655
|
Creates an image given a prompt.
|
|
@@ -564,6 +657,10 @@ def image_generations(
|
|
|
564
657
|
Equivalent to the OpenAI `images/generations` API endpoint.
|
|
565
658
|
For additional details, see: [https://platform.openai.com/docs/guides/images](https://platform.openai.com/docs/guides/images)
|
|
566
659
|
|
|
660
|
+
Request throttling:
|
|
661
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
662
|
+
limit is configured, uses a default of 600 RPM.
|
|
663
|
+
|
|
567
664
|
__Requirements:__
|
|
568
665
|
|
|
569
666
|
- `pip install openai`
|
|
@@ -581,17 +678,18 @@ def image_generations(
|
|
|
581
678
|
Add a computed column that applies the model `dall-e-2` to an existing
|
|
582
679
|
Pixeltable column `tbl.text` of the table `tbl`:
|
|
583
680
|
|
|
584
|
-
>>> tbl
|
|
681
|
+
>>> tbl.add_computed_column(gen_image=image_generations(tbl.text, model='dall-e-2'))
|
|
585
682
|
"""
|
|
586
683
|
# TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
|
|
587
|
-
result =
|
|
684
|
+
result = await _openai_client().images.generate(
|
|
588
685
|
prompt=prompt,
|
|
589
686
|
model=_opt(model),
|
|
590
|
-
quality=_opt(quality),
|
|
591
|
-
size=_opt(size),
|
|
592
|
-
style=_opt(style),
|
|
687
|
+
quality=_opt(quality), # type: ignore
|
|
688
|
+
size=_opt(size), # type: ignore
|
|
689
|
+
style=_opt(style), # type: ignore
|
|
593
690
|
user=_opt(user),
|
|
594
691
|
response_format='b64_json',
|
|
692
|
+
timeout=_opt(timeout),
|
|
595
693
|
)
|
|
596
694
|
b64_str = result.data[0].b64_json
|
|
597
695
|
b64_bytes = base64.b64decode(b64_str)
|
|
@@ -608,7 +706,7 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
|
|
|
608
706
|
if x_pos == -1:
|
|
609
707
|
return pxt.ImageType()
|
|
610
708
|
try:
|
|
611
|
-
width, height = int(size[:x_pos]), int(size[x_pos + 1:])
|
|
709
|
+
width, height = int(size[:x_pos]), int(size[x_pos + 1 :])
|
|
612
710
|
except ValueError:
|
|
613
711
|
return pxt.ImageType()
|
|
614
712
|
return pxt.ImageType(size=(width, height))
|
|
@@ -619,13 +717,17 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
|
|
|
619
717
|
|
|
620
718
|
|
|
621
719
|
@pxt.udf
|
|
622
|
-
def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
720
|
+
async def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
623
721
|
"""
|
|
624
722
|
Classifies if text is potentially harmful.
|
|
625
723
|
|
|
626
724
|
Equivalent to the OpenAI `moderations` API endpoint.
|
|
627
725
|
For additional details, see: [https://platform.openai.com/docs/guides/moderation](https://platform.openai.com/docs/guides/moderation)
|
|
628
726
|
|
|
727
|
+
Request throttling:
|
|
728
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
729
|
+
limit is configured, uses a default of 600 RPM.
|
|
730
|
+
|
|
629
731
|
__Requirements:__
|
|
630
732
|
|
|
631
733
|
- `pip install openai`
|
|
@@ -643,22 +745,26 @@ def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
|
643
745
|
Add a computed column that applies the model `text-moderation-stable` to an existing
|
|
644
746
|
Pixeltable column `tbl.input` of the table `tbl`:
|
|
645
747
|
|
|
646
|
-
>>> tbl
|
|
748
|
+
>>> tbl.add_computed_column(moderations=moderations(tbl.text, model='text-moderation-stable'))
|
|
647
749
|
"""
|
|
648
|
-
result =
|
|
750
|
+
result = await _openai_client().moderations.create(input=input, model=_opt(model))
|
|
649
751
|
return result.dict()
|
|
650
752
|
|
|
651
753
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
754
|
+
@speech.resource_pool
|
|
755
|
+
@transcriptions.resource_pool
|
|
756
|
+
@translations.resource_pool
|
|
757
|
+
@image_generations.resource_pool
|
|
758
|
+
@moderations.resource_pool
|
|
759
|
+
def _(model: str) -> str:
|
|
760
|
+
return f'request-rate:openai:{model}'
|
|
761
|
+
|
|
762
|
+
|
|
655
763
|
@chat_completions.resource_pool
|
|
656
|
-
|
|
764
|
+
@vision.resource_pool
|
|
657
765
|
@embeddings.resource_pool
|
|
658
|
-
# @image_generations.resource_pool
|
|
659
|
-
# @moderations.resource_pool
|
|
660
766
|
def _(model: str) -> str:
|
|
661
|
-
return
|
|
767
|
+
return _rate_limits_pool(model)
|
|
662
768
|
|
|
663
769
|
|
|
664
770
|
def invoke_tools(tools: Tools, response: exprs.Expr) -> exprs.InlineDict:
|
|
@@ -672,9 +778,7 @@ def _openai_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
|
|
|
672
778
|
return None
|
|
673
779
|
openai_tool_calls = response['choices'][0]['message']['tool_calls']
|
|
674
780
|
return {
|
|
675
|
-
tool_call['function']['name']: {
|
|
676
|
-
'args': json.loads(tool_call['function']['arguments'])
|
|
677
|
-
}
|
|
781
|
+
tool_call['function']['name']: {'args': json.loads(tool_call['function']['arguments'])}
|
|
678
782
|
for tool_call in openai_tool_calls
|
|
679
783
|
}
|
|
680
784
|
|
|
@@ -684,6 +788,7 @@ _T = TypeVar('_T')
|
|
|
684
788
|
|
|
685
789
|
def _opt(arg: _T) -> Union[_T, 'openai.NotGiven']:
|
|
686
790
|
import openai
|
|
791
|
+
|
|
687
792
|
return arg if arg is not None else openai.NOT_GIVEN
|
|
688
793
|
|
|
689
794
|
|