pixeltable 0.3.2__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 +22 -12
- 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 +121 -101
- pixeltable/catalog/table_version.py +291 -142
- pixeltable/catalog/table_version_path.py +8 -5
- pixeltable/catalog/view.py +67 -26
- pixeltable/dataframe.py +102 -72
- pixeltable/env.py +20 -21
- 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 +13 -7
- pixeltable/exec/expr_eval/expr_eval_node.py +24 -15
- pixeltable/exec/expr_eval/globals.py +30 -7
- pixeltable/exec/expr_eval/row_buffer.py +5 -6
- pixeltable/exec/expr_eval/schedulers.py +151 -31
- 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 +32 -17
- 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 +16 -12
- 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 +201 -108
- 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 +60 -26
- 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 +2 -1
- pixeltable/io/label_studio.py +77 -68
- pixeltable/io/pandas.py +33 -9
- pixeltable/io/parquet.py +9 -12
- 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 +7 -1
- 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 +62 -54
- pixeltable/utils/arrow.py +1 -2
- pixeltable/utils/coco.py +16 -17
- pixeltable/utils/code.py +1 -1
- pixeltable/utils/console_output.py +6 -3
- pixeltable/utils/description_helper.py +7 -7
- pixeltable/utils/documents.py +3 -1
- pixeltable/utils/filecache.py +12 -7
- 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.2.dist-info → pixeltable-0.3.3.dist-info}/METADATA +6 -8
- pixeltable-0.3.3.dist-info/RECORD +163 -0
- pixeltable-0.3.2.dist-info/RECORD +0 -161
- {pixeltable-0.3.2.dist-info → pixeltable-0.3.3.dist-info}/LICENSE +0 -0
- {pixeltable-0.3.2.dist-info → pixeltable-0.3.3.dist-info}/WHEEL +0 -0
- {pixeltable-0.3.2.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,13 +94,13 @@ 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
|
|
117
100
|
# expired connections
|
|
118
101
|
# (AsyncConnectionPool._close_expired_connections() fails with ConnectionError when executing
|
|
119
|
-
# 'await connection.aclose()', which is
|
|
102
|
+
# 'await connection.aclose()', which is very likely a bug in AsyncConnectionPool)
|
|
120
103
|
openai.APIConnectionError,
|
|
121
|
-
|
|
122
104
|
# the following errors are retryable according to OpenAI's API documentation
|
|
123
105
|
openai.RateLimitError,
|
|
124
106
|
openai.APITimeoutError,
|
|
@@ -143,7 +125,7 @@ _header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d
|
|
|
143
125
|
def _parse_header_duration(duration_str):
|
|
144
126
|
match = _header_duration_pattern.match(duration_str)
|
|
145
127
|
if not match:
|
|
146
|
-
raise ValueError(
|
|
128
|
+
raise ValueError('Invalid duration format')
|
|
147
129
|
|
|
148
130
|
days = int(match.group(1) or 0)
|
|
149
131
|
hours = int(match.group(2) or 0)
|
|
@@ -151,17 +133,11 @@ def _parse_header_duration(duration_str):
|
|
|
151
133
|
minutes = int(match.group(4) or 0)
|
|
152
134
|
seconds = float(match.group(5) or 0)
|
|
153
135
|
|
|
154
|
-
return datetime.timedelta(
|
|
155
|
-
days=days,
|
|
156
|
-
hours=hours,
|
|
157
|
-
minutes=minutes,
|
|
158
|
-
seconds=seconds,
|
|
159
|
-
milliseconds=milliseconds
|
|
160
|
-
)
|
|
136
|
+
return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
|
|
161
137
|
|
|
162
138
|
|
|
163
139
|
def _get_header_info(
|
|
164
|
-
|
|
140
|
+
headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
|
|
165
141
|
) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
|
|
166
142
|
assert requests or tokens
|
|
167
143
|
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
@@ -194,8 +170,14 @@ def _get_header_info(
|
|
|
194
170
|
|
|
195
171
|
|
|
196
172
|
@pxt.udf
|
|
197
|
-
def speech(
|
|
198
|
-
|
|
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,
|
|
199
181
|
) -> pxt.Audio:
|
|
200
182
|
"""
|
|
201
183
|
Generates audio from the input text.
|
|
@@ -203,6 +185,10 @@ def speech(
|
|
|
203
185
|
Equivalent to the OpenAI `audio/speech` API endpoint.
|
|
204
186
|
For additional details, see: [https://platform.openai.com/docs/guides/text-to-speech](https://platform.openai.com/docs/guides/text-to-speech)
|
|
205
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
|
+
|
|
206
192
|
__Requirements:__
|
|
207
193
|
|
|
208
194
|
- `pip install openai`
|
|
@@ -222,10 +208,15 @@ def speech(
|
|
|
222
208
|
Add a computed column that applies the model `tts-1` to an existing Pixeltable column `tbl.text`
|
|
223
209
|
of the table `tbl`:
|
|
224
210
|
|
|
225
|
-
>>> tbl
|
|
211
|
+
>>> tbl.add_computed_column(audio=speech(tbl.text, model='tts-1', voice='nova'))
|
|
226
212
|
"""
|
|
227
|
-
content =
|
|
228
|
-
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),
|
|
229
220
|
)
|
|
230
221
|
ext = response_format or 'mp3'
|
|
231
222
|
output_filename = str(env.Env.get().tmp_dir / f'{uuid.uuid4()}.{ext}')
|
|
@@ -234,13 +225,14 @@ def speech(
|
|
|
234
225
|
|
|
235
226
|
|
|
236
227
|
@pxt.udf
|
|
237
|
-
def transcriptions(
|
|
228
|
+
async def transcriptions(
|
|
238
229
|
audio: pxt.Audio,
|
|
239
230
|
*,
|
|
240
231
|
model: str,
|
|
241
232
|
language: Optional[str] = None,
|
|
242
233
|
prompt: Optional[str] = None,
|
|
243
234
|
temperature: Optional[float] = None,
|
|
235
|
+
timeout: Optional[float] = None,
|
|
244
236
|
) -> dict:
|
|
245
237
|
"""
|
|
246
238
|
Transcribes audio into the input language.
|
|
@@ -248,6 +240,10 @@ def transcriptions(
|
|
|
248
240
|
Equivalent to the OpenAI `audio/transcriptions` API endpoint.
|
|
249
241
|
For additional details, see: [https://platform.openai.com/docs/guides/speech-to-text](https://platform.openai.com/docs/guides/speech-to-text)
|
|
250
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
|
+
|
|
251
247
|
__Requirements:__
|
|
252
248
|
|
|
253
249
|
- `pip install openai`
|
|
@@ -265,22 +261,28 @@ def transcriptions(
|
|
|
265
261
|
Add a computed column that applies the model `whisper-1` to an existing Pixeltable column `tbl.audio`
|
|
266
262
|
of the table `tbl`:
|
|
267
263
|
|
|
268
|
-
>>> tbl
|
|
264
|
+
>>> tbl.add_computed_column(transcription=transcriptions(tbl.audio, model='whisper-1', language='en'))
|
|
269
265
|
"""
|
|
270
266
|
file = pathlib.Path(audio)
|
|
271
|
-
transcription =
|
|
272
|
-
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),
|
|
273
274
|
)
|
|
274
275
|
return transcription.dict()
|
|
275
276
|
|
|
276
277
|
|
|
277
278
|
@pxt.udf
|
|
278
|
-
def translations(
|
|
279
|
+
async def translations(
|
|
279
280
|
audio: pxt.Audio,
|
|
280
281
|
*,
|
|
281
282
|
model: str,
|
|
282
283
|
prompt: Optional[str] = None,
|
|
283
|
-
temperature: Optional[float] = None
|
|
284
|
+
temperature: Optional[float] = None,
|
|
285
|
+
timeout: Optional[float] = None,
|
|
284
286
|
) -> dict:
|
|
285
287
|
"""
|
|
286
288
|
Translates audio into English.
|
|
@@ -288,6 +290,10 @@ def translations(
|
|
|
288
290
|
Equivalent to the OpenAI `audio/translations` API endpoint.
|
|
289
291
|
For additional details, see: [https://platform.openai.com/docs/guides/speech-to-text](https://platform.openai.com/docs/guides/speech-to-text)
|
|
290
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
|
+
|
|
291
297
|
__Requirements:__
|
|
292
298
|
|
|
293
299
|
- `pip install openai`
|
|
@@ -305,11 +311,11 @@ def translations(
|
|
|
305
311
|
Add a computed column that applies the model `whisper-1` to an existing Pixeltable column `tbl.audio`
|
|
306
312
|
of the table `tbl`:
|
|
307
313
|
|
|
308
|
-
>>> tbl
|
|
314
|
+
>>> tbl.add_computed_column(translation=translations(tbl.audio, model='whisper-1', language='en'))
|
|
309
315
|
"""
|
|
310
316
|
file = pathlib.Path(audio)
|
|
311
|
-
translation =
|
|
312
|
-
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)
|
|
313
319
|
)
|
|
314
320
|
return translation.dict()
|
|
315
321
|
|
|
@@ -319,7 +325,7 @@ def translations(
|
|
|
319
325
|
|
|
320
326
|
|
|
321
327
|
def _chat_completions_get_request_resources(
|
|
322
|
-
|
|
328
|
+
messages: list, max_tokens: Optional[int], n: Optional[int]
|
|
323
329
|
) -> dict[str, int]:
|
|
324
330
|
completion_tokens = n * max_tokens
|
|
325
331
|
|
|
@@ -328,7 +334,7 @@ def _chat_completions_get_request_resources(
|
|
|
328
334
|
num_tokens += 4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
|
|
329
335
|
for key, value in message.items():
|
|
330
336
|
num_tokens += len(value) / 4
|
|
331
|
-
if key ==
|
|
337
|
+
if key == 'name': # if there's a name, the role is omitted
|
|
332
338
|
num_tokens -= 1 # role is always required and always 1 token
|
|
333
339
|
num_tokens += 2 # every reply is primed with <im_start>assistant
|
|
334
340
|
return {'requests': 1, 'tokens': int(num_tokens) + completion_tokens}
|
|
@@ -354,6 +360,7 @@ async def chat_completions(
|
|
|
354
360
|
tools: Optional[list[dict]] = None,
|
|
355
361
|
tool_choice: Optional[dict] = None,
|
|
356
362
|
user: Optional[str] = None,
|
|
363
|
+
timeout: Optional[float] = None,
|
|
357
364
|
) -> dict:
|
|
358
365
|
"""
|
|
359
366
|
Creates a model response for the given chat conversation.
|
|
@@ -361,6 +368,10 @@ async def chat_completions(
|
|
|
361
368
|
Equivalent to the OpenAI `chat/completions` API endpoint.
|
|
362
369
|
For additional details, see: [https://platform.openai.com/docs/guides/chat-completions](https://platform.openai.com/docs/guides/chat-completions)
|
|
363
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
|
+
|
|
364
375
|
__Requirements:__
|
|
365
376
|
|
|
366
377
|
- `pip install openai`
|
|
@@ -382,16 +393,10 @@ async def chat_completions(
|
|
|
382
393
|
{'role': 'system', 'content': 'You are a helpful assistant.'},
|
|
383
394
|
{'role': 'user', 'content': tbl.prompt}
|
|
384
395
|
]
|
|
385
|
-
tbl
|
|
396
|
+
tbl.add_computed_column(response=chat_completions(messages, model='gpt-4o-mini'))
|
|
386
397
|
"""
|
|
387
398
|
if tools is not None:
|
|
388
|
-
tools = [
|
|
389
|
-
{
|
|
390
|
-
'type': 'function',
|
|
391
|
-
'function': tool
|
|
392
|
-
}
|
|
393
|
-
for tool in tools
|
|
394
|
-
]
|
|
399
|
+
tools = [{'type': 'function', 'function': tool} for tool in tools]
|
|
395
400
|
|
|
396
401
|
tool_choice_: Union[str, dict, None] = None
|
|
397
402
|
if tool_choice is not None:
|
|
@@ -401,22 +406,20 @@ async def chat_completions(
|
|
|
401
406
|
tool_choice_ = 'required'
|
|
402
407
|
else:
|
|
403
408
|
assert tool_choice['tool'] is not None
|
|
404
|
-
tool_choice_ = {
|
|
405
|
-
'type': 'function',
|
|
406
|
-
'function': {'name': tool_choice['tool']}
|
|
407
|
-
}
|
|
409
|
+
tool_choice_ = {'type': 'function', 'function': {'name': tool_choice['tool']}}
|
|
408
410
|
|
|
409
411
|
extra_body: Optional[dict[str, Any]] = None
|
|
410
412
|
if tool_choice is not None and not tool_choice['parallel_tool_calls']:
|
|
411
413
|
extra_body = {'parallel_tool_calls': False}
|
|
412
414
|
|
|
413
415
|
# make sure the pool info exists prior to making the request
|
|
414
|
-
resource_pool =
|
|
416
|
+
resource_pool = _rate_limits_pool(model)
|
|
415
417
|
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
416
|
-
resource_pool, lambda: OpenAIRateLimitsInfo(_chat_completions_get_request_resources)
|
|
418
|
+
resource_pool, lambda: OpenAIRateLimitsInfo(_chat_completions_get_request_resources)
|
|
419
|
+
)
|
|
417
420
|
|
|
418
421
|
# cast(Any, ...): avoid mypy errors
|
|
419
|
-
result = await
|
|
422
|
+
result = await _openai_client().chat.completions.with_raw_response.create(
|
|
420
423
|
messages=messages,
|
|
421
424
|
model=model,
|
|
422
425
|
frequency_penalty=_opt(frequency_penalty),
|
|
@@ -434,7 +437,7 @@ async def chat_completions(
|
|
|
434
437
|
tools=_opt(cast(Any, tools)),
|
|
435
438
|
tool_choice=_opt(cast(Any, tool_choice_)),
|
|
436
439
|
user=_opt(user),
|
|
437
|
-
timeout=
|
|
440
|
+
timeout=_opt(timeout),
|
|
438
441
|
extra_body=extra_body,
|
|
439
442
|
)
|
|
440
443
|
|
|
@@ -444,14 +447,55 @@ async def chat_completions(
|
|
|
444
447
|
return json.loads(result.text)
|
|
445
448
|
|
|
446
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
|
+
|
|
447
479
|
@pxt.udf
|
|
448
|
-
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:
|
|
449
489
|
"""
|
|
450
490
|
Analyzes an image with the OpenAI vision capability. This is a convenience function that takes an image and
|
|
451
491
|
prompt, and constructs a chat completion request that utilizes OpenAI vision.
|
|
452
492
|
|
|
453
493
|
For additional details, see: [https://platform.openai.com/docs/guides/vision](https://platform.openai.com/docs/guides/vision)
|
|
454
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
|
+
|
|
455
499
|
__Requirements:__
|
|
456
500
|
|
|
457
501
|
- `pip install openai`
|
|
@@ -468,7 +512,7 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
|
|
|
468
512
|
Add a computed column that applies the model `gpt-4o-mini` to an existing Pixeltable column `tbl.image`
|
|
469
513
|
of the table `tbl`:
|
|
470
514
|
|
|
471
|
-
>>> tbl
|
|
515
|
+
>>> tbl.add_computed_column(response=vision("What's in this image?", tbl.image, model='gpt-4o-mini'))
|
|
472
516
|
"""
|
|
473
517
|
# TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
|
|
474
518
|
bytes_arr = io.BytesIO()
|
|
@@ -484,8 +528,25 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
|
|
|
484
528
|
],
|
|
485
529
|
}
|
|
486
530
|
]
|
|
487
|
-
|
|
488
|
-
|
|
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']
|
|
489
550
|
|
|
490
551
|
|
|
491
552
|
#####################################
|
|
@@ -505,7 +566,12 @@ def _embeddings_get_request_resources(input: list[str]) -> dict[str, int]:
|
|
|
505
566
|
|
|
506
567
|
@pxt.udf(batch_size=32)
|
|
507
568
|
async def embeddings(
|
|
508
|
-
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,
|
|
509
575
|
) -> Batch[pxt.Array[(None,), pxt.Float]]:
|
|
510
576
|
"""
|
|
511
577
|
Creates an embedding vector representing the input text.
|
|
@@ -513,6 +579,10 @@ async def embeddings(
|
|
|
513
579
|
Equivalent to the OpenAI `embeddings` API endpoint.
|
|
514
580
|
For additional details, see: [https://platform.openai.com/docs/guides/embeddings](https://platform.openai.com/docs/guides/embeddings)
|
|
515
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
|
+
|
|
516
586
|
__Requirements:__
|
|
517
587
|
|
|
518
588
|
- `pip install openai`
|
|
@@ -532,14 +602,24 @@ async def embeddings(
|
|
|
532
602
|
Add a computed column that applies the model `text-embedding-3-small` to an existing
|
|
533
603
|
Pixeltable column `tbl.text` of the table `tbl`:
|
|
534
604
|
|
|
535
|
-
>>> 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'))
|
|
536
610
|
"""
|
|
537
611
|
_logger.debug(f'embeddings: batch_size={len(input)}')
|
|
538
|
-
resource_pool =
|
|
612
|
+
resource_pool = _rate_limits_pool(model)
|
|
539
613
|
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
540
|
-
resource_pool, lambda: OpenAIRateLimitsInfo(_embeddings_get_request_resources)
|
|
541
|
-
|
|
542
|
-
|
|
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),
|
|
543
623
|
)
|
|
544
624
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
545
625
|
rate_limits_info.record(requests=requests_info, tokens=tokens_info)
|
|
@@ -561,7 +641,7 @@ def _(model: str, dimensions: Optional[int] = None) -> pxt.ArrayType:
|
|
|
561
641
|
|
|
562
642
|
|
|
563
643
|
@pxt.udf
|
|
564
|
-
def image_generations(
|
|
644
|
+
async def image_generations(
|
|
565
645
|
prompt: str,
|
|
566
646
|
*,
|
|
567
647
|
model: str = 'dall-e-2',
|
|
@@ -569,6 +649,7 @@ def image_generations(
|
|
|
569
649
|
size: Optional[str] = None,
|
|
570
650
|
style: Optional[str] = None,
|
|
571
651
|
user: Optional[str] = None,
|
|
652
|
+
timeout: Optional[float] = None,
|
|
572
653
|
) -> PIL.Image.Image:
|
|
573
654
|
"""
|
|
574
655
|
Creates an image given a prompt.
|
|
@@ -576,6 +657,10 @@ def image_generations(
|
|
|
576
657
|
Equivalent to the OpenAI `images/generations` API endpoint.
|
|
577
658
|
For additional details, see: [https://platform.openai.com/docs/guides/images](https://platform.openai.com/docs/guides/images)
|
|
578
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
|
+
|
|
579
664
|
__Requirements:__
|
|
580
665
|
|
|
581
666
|
- `pip install openai`
|
|
@@ -593,17 +678,18 @@ def image_generations(
|
|
|
593
678
|
Add a computed column that applies the model `dall-e-2` to an existing
|
|
594
679
|
Pixeltable column `tbl.text` of the table `tbl`:
|
|
595
680
|
|
|
596
|
-
>>> tbl
|
|
681
|
+
>>> tbl.add_computed_column(gen_image=image_generations(tbl.text, model='dall-e-2'))
|
|
597
682
|
"""
|
|
598
683
|
# TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
|
|
599
|
-
result =
|
|
684
|
+
result = await _openai_client().images.generate(
|
|
600
685
|
prompt=prompt,
|
|
601
686
|
model=_opt(model),
|
|
602
|
-
quality=_opt(quality),
|
|
603
|
-
size=_opt(size),
|
|
604
|
-
style=_opt(style),
|
|
687
|
+
quality=_opt(quality), # type: ignore
|
|
688
|
+
size=_opt(size), # type: ignore
|
|
689
|
+
style=_opt(style), # type: ignore
|
|
605
690
|
user=_opt(user),
|
|
606
691
|
response_format='b64_json',
|
|
692
|
+
timeout=_opt(timeout),
|
|
607
693
|
)
|
|
608
694
|
b64_str = result.data[0].b64_json
|
|
609
695
|
b64_bytes = base64.b64decode(b64_str)
|
|
@@ -620,7 +706,7 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
|
|
|
620
706
|
if x_pos == -1:
|
|
621
707
|
return pxt.ImageType()
|
|
622
708
|
try:
|
|
623
|
-
width, height = int(size[:x_pos]), int(size[x_pos + 1:])
|
|
709
|
+
width, height = int(size[:x_pos]), int(size[x_pos + 1 :])
|
|
624
710
|
except ValueError:
|
|
625
711
|
return pxt.ImageType()
|
|
626
712
|
return pxt.ImageType(size=(width, height))
|
|
@@ -631,13 +717,17 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
|
|
|
631
717
|
|
|
632
718
|
|
|
633
719
|
@pxt.udf
|
|
634
|
-
def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
720
|
+
async def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
635
721
|
"""
|
|
636
722
|
Classifies if text is potentially harmful.
|
|
637
723
|
|
|
638
724
|
Equivalent to the OpenAI `moderations` API endpoint.
|
|
639
725
|
For additional details, see: [https://platform.openai.com/docs/guides/moderation](https://platform.openai.com/docs/guides/moderation)
|
|
640
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
|
+
|
|
641
731
|
__Requirements:__
|
|
642
732
|
|
|
643
733
|
- `pip install openai`
|
|
@@ -655,22 +745,26 @@ def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
|
655
745
|
Add a computed column that applies the model `text-moderation-stable` to an existing
|
|
656
746
|
Pixeltable column `tbl.input` of the table `tbl`:
|
|
657
747
|
|
|
658
|
-
>>> tbl
|
|
748
|
+
>>> tbl.add_computed_column(moderations=moderations(tbl.text, model='text-moderation-stable'))
|
|
659
749
|
"""
|
|
660
|
-
result =
|
|
750
|
+
result = await _openai_client().moderations.create(input=input, model=_opt(model))
|
|
661
751
|
return result.dict()
|
|
662
752
|
|
|
663
753
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
+
|
|
667
763
|
@chat_completions.resource_pool
|
|
668
|
-
|
|
764
|
+
@vision.resource_pool
|
|
669
765
|
@embeddings.resource_pool
|
|
670
|
-
# @image_generations.resource_pool
|
|
671
|
-
# @moderations.resource_pool
|
|
672
766
|
def _(model: str) -> str:
|
|
673
|
-
return
|
|
767
|
+
return _rate_limits_pool(model)
|
|
674
768
|
|
|
675
769
|
|
|
676
770
|
def invoke_tools(tools: Tools, response: exprs.Expr) -> exprs.InlineDict:
|
|
@@ -684,9 +778,7 @@ def _openai_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
|
|
|
684
778
|
return None
|
|
685
779
|
openai_tool_calls = response['choices'][0]['message']['tool_calls']
|
|
686
780
|
return {
|
|
687
|
-
tool_call['function']['name']: {
|
|
688
|
-
'args': json.loads(tool_call['function']['arguments'])
|
|
689
|
-
}
|
|
781
|
+
tool_call['function']['name']: {'args': json.loads(tool_call['function']['arguments'])}
|
|
690
782
|
for tool_call in openai_tool_calls
|
|
691
783
|
}
|
|
692
784
|
|
|
@@ -696,6 +788,7 @@ _T = TypeVar('_T')
|
|
|
696
788
|
|
|
697
789
|
def _opt(arg: _T) -> Union[_T, 'openai.NotGiven']:
|
|
698
790
|
import openai
|
|
791
|
+
|
|
699
792
|
return arg if arg is not None else openai.NOT_GIVEN
|
|
700
793
|
|
|
701
794
|
|