pixeltable 0.4.0rc3__py3-none-any.whl → 0.4.20__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 +23 -5
- pixeltable/_version.py +1 -0
- pixeltable/catalog/__init__.py +5 -3
- pixeltable/catalog/catalog.py +1318 -404
- pixeltable/catalog/column.py +186 -115
- pixeltable/catalog/dir.py +1 -2
- pixeltable/catalog/globals.py +11 -43
- pixeltable/catalog/insertable_table.py +167 -79
- pixeltable/catalog/path.py +61 -23
- pixeltable/catalog/schema_object.py +9 -10
- pixeltable/catalog/table.py +626 -308
- pixeltable/catalog/table_metadata.py +101 -0
- pixeltable/catalog/table_version.py +713 -569
- pixeltable/catalog/table_version_handle.py +37 -6
- pixeltable/catalog/table_version_path.py +42 -29
- pixeltable/catalog/tbl_ops.py +50 -0
- pixeltable/catalog/update_status.py +191 -0
- pixeltable/catalog/view.py +108 -94
- pixeltable/config.py +128 -22
- pixeltable/dataframe.py +188 -100
- pixeltable/env.py +407 -136
- pixeltable/exceptions.py +6 -0
- pixeltable/exec/__init__.py +3 -0
- pixeltable/exec/aggregation_node.py +7 -8
- pixeltable/exec/cache_prefetch_node.py +83 -110
- pixeltable/exec/cell_materialization_node.py +231 -0
- pixeltable/exec/cell_reconstruction_node.py +135 -0
- pixeltable/exec/component_iteration_node.py +4 -3
- pixeltable/exec/data_row_batch.py +8 -65
- pixeltable/exec/exec_context.py +16 -4
- pixeltable/exec/exec_node.py +13 -36
- pixeltable/exec/expr_eval/evaluators.py +7 -6
- pixeltable/exec/expr_eval/expr_eval_node.py +27 -12
- pixeltable/exec/expr_eval/globals.py +8 -5
- pixeltable/exec/expr_eval/row_buffer.py +1 -2
- pixeltable/exec/expr_eval/schedulers.py +190 -30
- pixeltable/exec/globals.py +32 -0
- pixeltable/exec/in_memory_data_node.py +18 -18
- pixeltable/exec/object_store_save_node.py +293 -0
- pixeltable/exec/row_update_node.py +16 -9
- pixeltable/exec/sql_node.py +206 -101
- pixeltable/exprs/__init__.py +1 -1
- pixeltable/exprs/arithmetic_expr.py +27 -22
- pixeltable/exprs/array_slice.py +3 -3
- pixeltable/exprs/column_property_ref.py +34 -30
- pixeltable/exprs/column_ref.py +92 -96
- pixeltable/exprs/comparison.py +5 -5
- pixeltable/exprs/compound_predicate.py +5 -4
- pixeltable/exprs/data_row.py +152 -55
- pixeltable/exprs/expr.py +62 -43
- pixeltable/exprs/expr_dict.py +3 -3
- pixeltable/exprs/expr_set.py +17 -10
- pixeltable/exprs/function_call.py +75 -37
- pixeltable/exprs/globals.py +1 -2
- pixeltable/exprs/in_predicate.py +4 -4
- pixeltable/exprs/inline_expr.py +10 -27
- pixeltable/exprs/is_null.py +1 -3
- pixeltable/exprs/json_mapper.py +8 -8
- pixeltable/exprs/json_path.py +56 -22
- pixeltable/exprs/literal.py +5 -5
- pixeltable/exprs/method_ref.py +2 -2
- pixeltable/exprs/object_ref.py +2 -2
- pixeltable/exprs/row_builder.py +127 -53
- pixeltable/exprs/rowid_ref.py +8 -12
- pixeltable/exprs/similarity_expr.py +50 -25
- pixeltable/exprs/sql_element_cache.py +4 -4
- pixeltable/exprs/string_op.py +5 -5
- pixeltable/exprs/type_cast.py +3 -5
- pixeltable/func/__init__.py +1 -0
- pixeltable/func/aggregate_function.py +8 -8
- pixeltable/func/callable_function.py +9 -9
- pixeltable/func/expr_template_function.py +10 -10
- pixeltable/func/function.py +18 -20
- pixeltable/func/function_registry.py +6 -7
- pixeltable/func/globals.py +2 -3
- pixeltable/func/mcp.py +74 -0
- pixeltable/func/query_template_function.py +20 -18
- pixeltable/func/signature.py +43 -16
- pixeltable/func/tools.py +23 -13
- pixeltable/func/udf.py +18 -20
- pixeltable/functions/__init__.py +6 -0
- pixeltable/functions/anthropic.py +93 -33
- pixeltable/functions/audio.py +114 -10
- pixeltable/functions/bedrock.py +13 -6
- pixeltable/functions/date.py +1 -1
- pixeltable/functions/deepseek.py +20 -9
- pixeltable/functions/fireworks.py +2 -2
- pixeltable/functions/gemini.py +28 -11
- pixeltable/functions/globals.py +13 -13
- pixeltable/functions/groq.py +108 -0
- pixeltable/functions/huggingface.py +1046 -23
- pixeltable/functions/image.py +9 -18
- pixeltable/functions/llama_cpp.py +23 -8
- pixeltable/functions/math.py +3 -4
- pixeltable/functions/mistralai.py +4 -15
- pixeltable/functions/ollama.py +16 -9
- pixeltable/functions/openai.py +104 -82
- pixeltable/functions/openrouter.py +143 -0
- pixeltable/functions/replicate.py +2 -2
- pixeltable/functions/reve.py +250 -0
- pixeltable/functions/string.py +21 -28
- pixeltable/functions/timestamp.py +13 -14
- pixeltable/functions/together.py +4 -6
- pixeltable/functions/twelvelabs.py +92 -0
- pixeltable/functions/util.py +6 -1
- pixeltable/functions/video.py +1388 -106
- pixeltable/functions/vision.py +7 -7
- pixeltable/functions/whisper.py +15 -7
- pixeltable/functions/whisperx.py +179 -0
- pixeltable/{ext/functions → functions}/yolox.py +2 -4
- pixeltable/globals.py +332 -105
- pixeltable/index/base.py +13 -22
- pixeltable/index/btree.py +23 -22
- pixeltable/index/embedding_index.py +32 -44
- pixeltable/io/__init__.py +4 -2
- pixeltable/io/datarows.py +7 -6
- pixeltable/io/external_store.py +49 -77
- pixeltable/io/fiftyone.py +11 -11
- pixeltable/io/globals.py +29 -28
- pixeltable/io/hf_datasets.py +17 -9
- pixeltable/io/label_studio.py +70 -66
- pixeltable/io/lancedb.py +3 -0
- pixeltable/io/pandas.py +12 -11
- pixeltable/io/parquet.py +13 -93
- pixeltable/io/table_data_conduit.py +71 -47
- pixeltable/io/utils.py +3 -3
- pixeltable/iterators/__init__.py +2 -1
- pixeltable/iterators/audio.py +21 -11
- pixeltable/iterators/document.py +116 -55
- pixeltable/iterators/image.py +5 -2
- pixeltable/iterators/video.py +293 -13
- pixeltable/metadata/__init__.py +4 -2
- pixeltable/metadata/converters/convert_18.py +2 -2
- pixeltable/metadata/converters/convert_19.py +2 -2
- pixeltable/metadata/converters/convert_20.py +2 -2
- pixeltable/metadata/converters/convert_21.py +2 -2
- pixeltable/metadata/converters/convert_22.py +2 -2
- pixeltable/metadata/converters/convert_24.py +2 -2
- pixeltable/metadata/converters/convert_25.py +2 -2
- pixeltable/metadata/converters/convert_26.py +2 -2
- pixeltable/metadata/converters/convert_29.py +4 -4
- pixeltable/metadata/converters/convert_34.py +2 -2
- pixeltable/metadata/converters/convert_36.py +2 -2
- pixeltable/metadata/converters/convert_37.py +15 -0
- pixeltable/metadata/converters/convert_38.py +39 -0
- pixeltable/metadata/converters/convert_39.py +124 -0
- pixeltable/metadata/converters/convert_40.py +73 -0
- pixeltable/metadata/converters/util.py +13 -12
- pixeltable/metadata/notes.py +4 -0
- pixeltable/metadata/schema.py +79 -42
- pixeltable/metadata/utils.py +74 -0
- pixeltable/mypy/__init__.py +3 -0
- pixeltable/mypy/mypy_plugin.py +123 -0
- pixeltable/plan.py +274 -223
- pixeltable/share/__init__.py +1 -1
- pixeltable/share/packager.py +259 -129
- pixeltable/share/protocol/__init__.py +34 -0
- pixeltable/share/protocol/common.py +170 -0
- pixeltable/share/protocol/operation_types.py +33 -0
- pixeltable/share/protocol/replica.py +109 -0
- pixeltable/share/publish.py +213 -57
- pixeltable/store.py +238 -175
- pixeltable/type_system.py +104 -63
- pixeltable/utils/__init__.py +2 -3
- pixeltable/utils/arrow.py +108 -13
- pixeltable/utils/av.py +298 -0
- pixeltable/utils/azure_store.py +305 -0
- pixeltable/utils/code.py +3 -3
- pixeltable/utils/console_output.py +4 -1
- pixeltable/utils/coroutine.py +6 -23
- pixeltable/utils/dbms.py +31 -5
- pixeltable/utils/description_helper.py +4 -5
- pixeltable/utils/documents.py +5 -6
- pixeltable/utils/exception_handler.py +7 -30
- pixeltable/utils/filecache.py +6 -6
- pixeltable/utils/formatter.py +4 -6
- pixeltable/utils/gcs_store.py +283 -0
- pixeltable/utils/http_server.py +2 -3
- pixeltable/utils/iceberg.py +1 -2
- pixeltable/utils/image.py +17 -0
- pixeltable/utils/lancedb.py +88 -0
- pixeltable/utils/local_store.py +316 -0
- pixeltable/utils/misc.py +5 -0
- pixeltable/utils/object_stores.py +528 -0
- pixeltable/utils/pydantic.py +60 -0
- pixeltable/utils/pytorch.py +5 -6
- pixeltable/utils/s3_store.py +392 -0
- pixeltable-0.4.20.dist-info/METADATA +587 -0
- pixeltable-0.4.20.dist-info/RECORD +218 -0
- {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.20.dist-info}/WHEEL +1 -1
- pixeltable-0.4.20.dist-info/entry_points.txt +2 -0
- pixeltable/__version__.py +0 -3
- pixeltable/ext/__init__.py +0 -17
- pixeltable/ext/functions/__init__.py +0 -11
- pixeltable/ext/functions/whisperx.py +0 -77
- pixeltable/utils/media_store.py +0 -77
- pixeltable/utils/s3.py +0 -17
- pixeltable/utils/sample.py +0 -25
- pixeltable-0.4.0rc3.dist-info/METADATA +0 -435
- pixeltable-0.4.0rc3.dist-info/RECORD +0 -189
- pixeltable-0.4.0rc3.dist-info/entry_points.txt +0 -3
- {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.20.dist-info/licenses}/LICENSE +0 -0
pixeltable/functions/image.py
CHANGED
|
@@ -10,15 +10,13 @@ t.select(t.img_col.convert('L')).collect()
|
|
|
10
10
|
```
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import base64
|
|
14
|
-
from typing import Optional
|
|
15
|
-
|
|
16
13
|
import PIL.Image
|
|
17
14
|
|
|
18
15
|
import pixeltable as pxt
|
|
19
16
|
import pixeltable.type_system as ts
|
|
20
17
|
from pixeltable.exprs import Expr
|
|
21
18
|
from pixeltable.utils.code import local_public_names
|
|
19
|
+
from pixeltable.utils.image import to_base64
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
@pxt.udf(is_method=True)
|
|
@@ -30,12 +28,7 @@ def b64_encode(img: PIL.Image.Image, image_format: str = 'png') -> str:
|
|
|
30
28
|
img: image
|
|
31
29
|
image_format: image format [supported by PIL](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#fully-supported-formats)
|
|
32
30
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
bytes_arr = io.BytesIO()
|
|
36
|
-
img.save(bytes_arr, format=image_format)
|
|
37
|
-
b64_bytes = base64.b64encode(bytes_arr.getvalue())
|
|
38
|
-
return b64_bytes.decode('utf-8')
|
|
31
|
+
return to_base64(img, format=image_format)
|
|
39
32
|
|
|
40
33
|
|
|
41
34
|
@pxt.udf(substitute_fn=PIL.Image.alpha_composite, is_method=True)
|
|
@@ -156,7 +149,7 @@ def get_metadata(self: PIL.Image.Image) -> dict:
|
|
|
156
149
|
|
|
157
150
|
# Image.point()
|
|
158
151
|
@pxt.udf(is_method=True)
|
|
159
|
-
def point(self: PIL.Image.Image, lut: list[int], mode:
|
|
152
|
+
def point(self: PIL.Image.Image, lut: list[int], mode: str | None = None) -> PIL.Image.Image:
|
|
160
153
|
"""
|
|
161
154
|
Map image pixels through a lookup table.
|
|
162
155
|
|
|
@@ -241,7 +234,7 @@ def _(self: Expr) -> ts.ColumnType:
|
|
|
241
234
|
|
|
242
235
|
|
|
243
236
|
@pxt.udf(substitute_fn=PIL.Image.Image.entropy, is_method=True)
|
|
244
|
-
def entropy(self: PIL.Image.Image, mask:
|
|
237
|
+
def entropy(self: PIL.Image.Image, mask: PIL.Image.Image | None = None, extrema: list | None = None) -> float:
|
|
245
238
|
"""
|
|
246
239
|
Returns the entropy of the image, optionally using a mask and extrema.
|
|
247
240
|
|
|
@@ -306,7 +299,7 @@ def getextrema(self: PIL.Image.Image) -> tuple[int, int]:
|
|
|
306
299
|
|
|
307
300
|
|
|
308
301
|
@pxt.udf(substitute_fn=PIL.Image.Image.getpalette, is_method=True)
|
|
309
|
-
def getpalette(self: PIL.Image.Image, mode:
|
|
302
|
+
def getpalette(self: PIL.Image.Image, mode: str | None = None) -> tuple[int]:
|
|
310
303
|
"""
|
|
311
304
|
Return the palette of the image, optionally converting it to a different mode.
|
|
312
305
|
|
|
@@ -346,9 +339,7 @@ def getprojection(self: PIL.Image.Image) -> tuple[int]:
|
|
|
346
339
|
|
|
347
340
|
|
|
348
341
|
@pxt.udf(substitute_fn=PIL.Image.Image.histogram, is_method=True)
|
|
349
|
-
def histogram(
|
|
350
|
-
self: PIL.Image.Image, mask: Optional[PIL.Image.Image] = None, extrema: Optional[list] = None
|
|
351
|
-
) -> list[int]:
|
|
342
|
+
def histogram(self: PIL.Image.Image, mask: PIL.Image.Image | None = None, extrema: list | None = None) -> list[int]:
|
|
352
343
|
"""
|
|
353
344
|
Return a histogram for the image.
|
|
354
345
|
|
|
@@ -366,9 +357,9 @@ def histogram(
|
|
|
366
357
|
def quantize(
|
|
367
358
|
self: PIL.Image.Image,
|
|
368
359
|
colors: int = 256,
|
|
369
|
-
method:
|
|
360
|
+
method: int | None = None,
|
|
370
361
|
kmeans: int = 0,
|
|
371
|
-
palette:
|
|
362
|
+
palette: int | None = None,
|
|
372
363
|
dither: int = PIL.Image.Dither.FLOYDSTEINBERG,
|
|
373
364
|
) -> PIL.Image.Image:
|
|
374
365
|
"""
|
|
@@ -392,7 +383,7 @@ def quantize(
|
|
|
392
383
|
|
|
393
384
|
|
|
394
385
|
@pxt.udf(substitute_fn=PIL.Image.Image.reduce, is_method=True)
|
|
395
|
-
def reduce(self: PIL.Image.Image, factor: int, box:
|
|
386
|
+
def reduce(self: PIL.Image.Image, factor: int, box: tuple[int, int, int, int] | None = None) -> PIL.Image.Image:
|
|
396
387
|
"""
|
|
397
388
|
Reduce the image by the given factor.
|
|
398
389
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pixeltable UDFs for llama.cpp models.
|
|
3
|
+
|
|
4
|
+
Provides integration with llama.cpp for running quantized language models locally,
|
|
5
|
+
supporting chat completions and embeddings with GGUF format models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from pathlib import Path
|
|
2
|
-
from typing import TYPE_CHECKING, Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
3
10
|
|
|
4
11
|
import pixeltable as pxt
|
|
5
12
|
import pixeltable.exceptions as excs
|
|
@@ -14,10 +21,10 @@ if TYPE_CHECKING:
|
|
|
14
21
|
def create_chat_completion(
|
|
15
22
|
messages: list[dict],
|
|
16
23
|
*,
|
|
17
|
-
model_path:
|
|
18
|
-
repo_id:
|
|
19
|
-
repo_filename:
|
|
20
|
-
model_kwargs:
|
|
24
|
+
model_path: str | None = None,
|
|
25
|
+
repo_id: str | None = None,
|
|
26
|
+
repo_filename: str | None = None,
|
|
27
|
+
model_kwargs: dict[str, Any] | None = None,
|
|
21
28
|
) -> dict:
|
|
22
29
|
"""
|
|
23
30
|
Generate a chat completion from a list of messages.
|
|
@@ -81,7 +88,7 @@ def _lookup_local_model(model_path: str, n_gpu_layers: int) -> 'llama_cpp.Llama'
|
|
|
81
88
|
return _model_cache[key]
|
|
82
89
|
|
|
83
90
|
|
|
84
|
-
def _lookup_pretrained_model(repo_id: str, filename:
|
|
91
|
+
def _lookup_pretrained_model(repo_id: str, filename: str | None, n_gpu_layers: int) -> 'llama_cpp.Llama':
|
|
85
92
|
import llama_cpp
|
|
86
93
|
|
|
87
94
|
key = (repo_id, filename, n_gpu_layers)
|
|
@@ -93,8 +100,16 @@ def _lookup_pretrained_model(repo_id: str, filename: Optional[str], n_gpu_layers
|
|
|
93
100
|
return _model_cache[key]
|
|
94
101
|
|
|
95
102
|
|
|
96
|
-
_model_cache: dict[tuple[str, str, int],
|
|
97
|
-
_IS_GPU_AVAILABLE:
|
|
103
|
+
_model_cache: dict[tuple[str, str, int], 'llama_cpp.Llama'] = {}
|
|
104
|
+
_IS_GPU_AVAILABLE: bool | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cleanup() -> None:
|
|
108
|
+
for model in _model_cache.values():
|
|
109
|
+
if model._sampler is not None:
|
|
110
|
+
model._sampler.close()
|
|
111
|
+
model.close()
|
|
112
|
+
_model_cache.clear()
|
|
98
113
|
|
|
99
114
|
|
|
100
115
|
__all__ = local_public_names(__name__)
|
pixeltable/functions/math.py
CHANGED
|
@@ -12,7 +12,6 @@ t.select(t.float_col.floor()).collect()
|
|
|
12
12
|
|
|
13
13
|
import builtins
|
|
14
14
|
import math
|
|
15
|
-
from typing import Optional
|
|
16
15
|
|
|
17
16
|
import sqlalchemy as sql
|
|
18
17
|
|
|
@@ -80,7 +79,7 @@ def _(self: sql.ColumnElement) -> sql.ColumnElement:
|
|
|
80
79
|
|
|
81
80
|
|
|
82
81
|
@pxt.udf(is_method=True)
|
|
83
|
-
def round(self: float, digits:
|
|
82
|
+
def round(self: float, digits: int | None = None) -> float:
|
|
84
83
|
"""
|
|
85
84
|
Round a number to a given precision in decimal digits.
|
|
86
85
|
|
|
@@ -93,11 +92,11 @@ def round(self: float, digits: Optional[int] = None) -> float:
|
|
|
93
92
|
|
|
94
93
|
|
|
95
94
|
@round.to_sql
|
|
96
|
-
def _(self: sql.ColumnElement, digits:
|
|
95
|
+
def _(self: sql.ColumnElement, digits: sql.ColumnElement | None = None) -> sql.ColumnElement:
|
|
97
96
|
if digits is None:
|
|
98
97
|
return sql.func.round(self)
|
|
99
98
|
else:
|
|
100
|
-
return sql.func.round(sql.cast(self, sql.Numeric), sql.cast(digits, sql.Integer))
|
|
99
|
+
return sql.cast(sql.func.round(sql.cast(self, sql.Numeric), sql.cast(digits, sql.Integer)), sql.Float)
|
|
101
100
|
|
|
102
101
|
|
|
103
102
|
@pxt.udf(is_method=True)
|
|
@@ -5,7 +5,7 @@ first `pip install mistralai` and configure your Mistral AI credentials, as desc
|
|
|
5
5
|
the [Working with Mistral AI](https://pixeltable.readme.io/docs/working-with-mistralai) tutorial.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ from pixeltable.func.signature import Batch
|
|
|
16
16
|
from pixeltable.utils.code import local_public_names
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
-
import mistralai
|
|
19
|
+
import mistralai
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@register_client('mistral')
|
|
@@ -32,7 +32,7 @@ def _mistralai_client() -> 'mistralai.Mistral':
|
|
|
32
32
|
|
|
33
33
|
@pxt.udf(resource_pool='request-rate:mistral')
|
|
34
34
|
async def chat_completions(
|
|
35
|
-
messages: list[dict[str, str]], *, model: str, model_kwargs:
|
|
35
|
+
messages: list[dict[str, str]], *, model: str, model_kwargs: dict[str, Any] | None = None
|
|
36
36
|
) -> dict:
|
|
37
37
|
"""
|
|
38
38
|
Chat Completion API.
|
|
@@ -54,8 +54,6 @@ async def chat_completions(
|
|
|
54
54
|
model_kwargs: Additional keyword args for the Mistral `chat/completions` API.
|
|
55
55
|
For details on the available parameters, see: <https://docs.mistral.ai/api/#tag/chat>
|
|
56
56
|
|
|
57
|
-
For details on the other parameters, see: <https://docs.mistral.ai/api/#tag/chat>
|
|
58
|
-
|
|
59
57
|
Returns:
|
|
60
58
|
A dictionary containing the response and other metadata.
|
|
61
59
|
|
|
@@ -79,7 +77,7 @@ async def chat_completions(
|
|
|
79
77
|
|
|
80
78
|
|
|
81
79
|
@pxt.udf(resource_pool='request-rate:mistral')
|
|
82
|
-
async def fim_completions(prompt: str, *, model: str, model_kwargs:
|
|
80
|
+
async def fim_completions(prompt: str, *, model: str, model_kwargs: dict[str, Any] | None = None) -> dict:
|
|
83
81
|
"""
|
|
84
82
|
Fill-in-the-middle Completion API.
|
|
85
83
|
|
|
@@ -156,15 +154,6 @@ def _(model: str) -> ts.ArrayType:
|
|
|
156
154
|
return ts.ArrayType((dimensions,), dtype=ts.FloatType())
|
|
157
155
|
|
|
158
156
|
|
|
159
|
-
_T = TypeVar('_T')
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _opt(arg: Optional[_T]) -> Union[_T, 'mistralai.types.basemodel.Unset']:
|
|
163
|
-
from mistralai.types import UNSET
|
|
164
|
-
|
|
165
|
-
return arg if arg is not None else UNSET
|
|
166
|
-
|
|
167
|
-
|
|
168
157
|
__all__ = local_public_names(__name__)
|
|
169
158
|
|
|
170
159
|
|
pixeltable/functions/ollama.py
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Pixeltable UDFs for Ollama local models.
|
|
3
|
+
|
|
4
|
+
Provides integration with Ollama for running large language models locally,
|
|
5
|
+
including chat completions and embeddings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
2
9
|
|
|
3
10
|
import numpy as np
|
|
4
11
|
|
|
@@ -18,7 +25,7 @@ def _(host: str) -> 'ollama.Client':
|
|
|
18
25
|
return ollama.Client(host=host)
|
|
19
26
|
|
|
20
27
|
|
|
21
|
-
def _ollama_client() ->
|
|
28
|
+
def _ollama_client() -> 'ollama.Client | None':
|
|
22
29
|
try:
|
|
23
30
|
return env.Env.get().get_client('ollama')
|
|
24
31
|
except Exception:
|
|
@@ -33,10 +40,10 @@ def generate(
|
|
|
33
40
|
suffix: str = '',
|
|
34
41
|
system: str = '',
|
|
35
42
|
template: str = '',
|
|
36
|
-
context:
|
|
43
|
+
context: list[int] | None = None,
|
|
37
44
|
raw: bool = False,
|
|
38
|
-
format:
|
|
39
|
-
options:
|
|
45
|
+
format: str | None = None,
|
|
46
|
+
options: dict | None = None,
|
|
40
47
|
) -> dict:
|
|
41
48
|
"""
|
|
42
49
|
Generate a response for a given prompt with a provided model.
|
|
@@ -77,9 +84,9 @@ def chat(
|
|
|
77
84
|
messages: list[dict],
|
|
78
85
|
*,
|
|
79
86
|
model: str,
|
|
80
|
-
tools:
|
|
81
|
-
format:
|
|
82
|
-
options:
|
|
87
|
+
tools: list[dict] | None = None,
|
|
88
|
+
format: str | None = None,
|
|
89
|
+
options: dict | None = None,
|
|
83
90
|
) -> dict:
|
|
84
91
|
"""
|
|
85
92
|
Generate the next message in a chat with a provided model.
|
|
@@ -103,7 +110,7 @@ def chat(
|
|
|
103
110
|
|
|
104
111
|
@pxt.udf(batch_size=16)
|
|
105
112
|
def embed(
|
|
106
|
-
input: Batch[str], *, model: str, truncate: bool = True, options:
|
|
113
|
+
input: Batch[str], *, model: str, truncate: bool = True, options: dict | None = None
|
|
107
114
|
) -> Batch[pxt.Array[(None,), pxt.Float]]:
|
|
108
115
|
"""
|
|
109
116
|
Generate embeddings from a model.
|
pixeltable/functions/openai.py
CHANGED
|
@@ -13,8 +13,7 @@ import logging
|
|
|
13
13
|
import math
|
|
14
14
|
import pathlib
|
|
15
15
|
import re
|
|
16
|
-
import
|
|
17
|
-
from typing import TYPE_CHECKING, Any, Callable, Optional, Type
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Callable, Type
|
|
18
17
|
|
|
19
18
|
import httpx
|
|
20
19
|
import numpy as np
|
|
@@ -24,6 +23,7 @@ import pixeltable as pxt
|
|
|
24
23
|
from pixeltable import env, exprs, type_system as ts
|
|
25
24
|
from pixeltable.func import Batch, Tools
|
|
26
25
|
from pixeltable.utils.code import local_public_names
|
|
26
|
+
from pixeltable.utils.local_store import TempStore
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
29
|
import openai
|
|
@@ -32,11 +32,15 @@ _logger = logging.getLogger('pixeltable')
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@env.register_client('openai')
|
|
35
|
-
def _(api_key: str) -> 'openai.AsyncOpenAI':
|
|
35
|
+
def _(api_key: str, base_url: str | None = None, api_version: str | None = None) -> 'openai.AsyncOpenAI':
|
|
36
36
|
import openai
|
|
37
37
|
|
|
38
|
+
default_query = None if api_version is None else {'api-version': api_version}
|
|
39
|
+
|
|
38
40
|
return openai.AsyncOpenAI(
|
|
39
41
|
api_key=api_key,
|
|
42
|
+
base_url=base_url,
|
|
43
|
+
default_query=default_query,
|
|
40
44
|
# recommended to increase limits for async client to avoid connection errors
|
|
41
45
|
http_client=httpx.AsyncClient(limits=httpx.Limits(max_keepalive_connections=100, max_connections=500)),
|
|
42
46
|
)
|
|
@@ -88,6 +92,52 @@ def _rate_limits_pool(model: str) -> str:
|
|
|
88
92
|
return f'rate-limits:openai:{model}'
|
|
89
93
|
|
|
90
94
|
|
|
95
|
+
# RE pattern for duration in '*-reset' headers;
|
|
96
|
+
# examples: 1d2h3ms, 4m5.6s; # fractional seconds can be reported as 0.5s or 500ms
|
|
97
|
+
_header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d+)m)?(?:([\d.]+)s)?')
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_header_duration(duration_str: str) -> datetime.timedelta:
|
|
101
|
+
match = _header_duration_pattern.match(duration_str)
|
|
102
|
+
if not match:
|
|
103
|
+
raise ValueError(f'Invalid duration format: {duration_str}')
|
|
104
|
+
|
|
105
|
+
days = int(match.group(1) or 0)
|
|
106
|
+
hours = int(match.group(2) or 0)
|
|
107
|
+
milliseconds = int(match.group(3) or 0)
|
|
108
|
+
minutes = int(match.group(4) or 0)
|
|
109
|
+
seconds = float(match.group(5) or 0)
|
|
110
|
+
|
|
111
|
+
return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _get_header_info(
|
|
115
|
+
headers: httpx.Headers,
|
|
116
|
+
) -> tuple[tuple[int, int, datetime.datetime] | None, tuple[int, int, datetime.datetime] | None]:
|
|
117
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
118
|
+
|
|
119
|
+
requests_limit_str = headers.get('x-ratelimit-limit-requests')
|
|
120
|
+
requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
|
|
121
|
+
requests_remaining_str = headers.get('x-ratelimit-remaining-requests')
|
|
122
|
+
requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
|
|
123
|
+
requests_reset_str = headers.get('x-ratelimit-reset-requests', '5s') # Default to 5 seconds
|
|
124
|
+
requests_reset_ts = now + _parse_header_duration(requests_reset_str)
|
|
125
|
+
requests_info = (requests_limit, requests_remaining, requests_reset_ts) if requests_remaining is not None else None
|
|
126
|
+
|
|
127
|
+
tokens_limit_str = headers.get('x-ratelimit-limit-tokens')
|
|
128
|
+
tokens_limit = int(tokens_limit_str) if tokens_limit_str is not None else None
|
|
129
|
+
tokens_remaining_str = headers.get('x-ratelimit-remaining-tokens')
|
|
130
|
+
tokens_remaining = int(tokens_remaining_str) if tokens_remaining_str is not None else None
|
|
131
|
+
tokens_reset_str = headers.get('x-ratelimit-reset-tokens', '5s') # Default to 5 seconds
|
|
132
|
+
tokens_reset_ts = now + _parse_header_duration(tokens_reset_str)
|
|
133
|
+
tokens_info = (tokens_limit, tokens_remaining, tokens_reset_ts) if tokens_remaining is not None else None
|
|
134
|
+
|
|
135
|
+
if requests_info is None or tokens_info is None:
|
|
136
|
+
_logger.debug(f'get_header_info(): incomplete rate limit info: {headers}')
|
|
137
|
+
|
|
138
|
+
return requests_info, tokens_info
|
|
139
|
+
|
|
140
|
+
|
|
91
141
|
class OpenAIRateLimitsInfo(env.RateLimitsInfo):
|
|
92
142
|
retryable_errors: tuple[Type[Exception], ...]
|
|
93
143
|
|
|
@@ -108,61 +158,24 @@ class OpenAIRateLimitsInfo(env.RateLimitsInfo):
|
|
|
108
158
|
openai.InternalServerError,
|
|
109
159
|
)
|
|
110
160
|
|
|
111
|
-
def
|
|
161
|
+
def record_exc(self, exc: Exception) -> None:
|
|
162
|
+
import openai
|
|
163
|
+
|
|
164
|
+
_ = isinstance(exc, openai.APIError)
|
|
165
|
+
if not isinstance(exc, openai.APIError) or not hasattr(exc, 'response') or not hasattr(exc.response, 'headers'):
|
|
166
|
+
return
|
|
167
|
+
requests_info, tokens_info = _get_header_info(exc.response.headers)
|
|
168
|
+
_logger.debug(f'record_exc(): requests_info={requests_info} tokens_info={tokens_info}')
|
|
169
|
+
self.record(requests=requests_info, tokens=tokens_info)
|
|
170
|
+
self.has_exc = True
|
|
171
|
+
|
|
172
|
+
def get_retry_delay(self, exc: Exception) -> float | None:
|
|
112
173
|
import openai
|
|
113
174
|
|
|
114
175
|
if not isinstance(exc, self.retryable_errors):
|
|
115
176
|
return None
|
|
116
177
|
assert isinstance(exc, openai.APIError)
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# RE pattern for duration in '*-reset' headers;
|
|
121
|
-
# examples: 1d2h3ms, 4m5.6s; # fractional seconds can be reported as 0.5s or 500ms
|
|
122
|
-
_header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d+)m)?(?:([\d.]+)s)?')
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _parse_header_duration(duration_str: str) -> datetime.timedelta:
|
|
126
|
-
match = _header_duration_pattern.match(duration_str)
|
|
127
|
-
if not match:
|
|
128
|
-
raise ValueError('Invalid duration format')
|
|
129
|
-
|
|
130
|
-
days = int(match.group(1) or 0)
|
|
131
|
-
hours = int(match.group(2) or 0)
|
|
132
|
-
milliseconds = int(match.group(3) or 0)
|
|
133
|
-
minutes = int(match.group(4) or 0)
|
|
134
|
-
seconds = float(match.group(5) or 0)
|
|
135
|
-
|
|
136
|
-
return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _get_header_info(
|
|
140
|
-
headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
|
|
141
|
-
) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
|
|
142
|
-
assert requests or tokens
|
|
143
|
-
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
144
|
-
|
|
145
|
-
requests_info: Optional[tuple[int, int, datetime.datetime]] = None
|
|
146
|
-
if requests:
|
|
147
|
-
requests_limit_str = headers.get('x-ratelimit-limit-requests')
|
|
148
|
-
requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
|
|
149
|
-
requests_remaining_str = headers.get('x-ratelimit-remaining-requests')
|
|
150
|
-
requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
|
|
151
|
-
requests_reset_str = headers.get('x-ratelimit-reset-requests')
|
|
152
|
-
requests_reset_ts = now + _parse_header_duration(requests_reset_str)
|
|
153
|
-
requests_info = (requests_limit, requests_remaining, requests_reset_ts)
|
|
154
|
-
|
|
155
|
-
tokens_info: Optional[tuple[int, int, datetime.datetime]] = None
|
|
156
|
-
if tokens:
|
|
157
|
-
tokens_limit_str = headers.get('x-ratelimit-limit-tokens')
|
|
158
|
-
tokens_limit = int(tokens_limit_str) if tokens_limit_str is not None else None
|
|
159
|
-
tokens_remaining_str = headers.get('x-ratelimit-remaining-tokens')
|
|
160
|
-
tokens_remaining = int(tokens_remaining_str) if tokens_remaining_str is not None else None
|
|
161
|
-
tokens_reset_str = headers.get('x-ratelimit-reset-tokens')
|
|
162
|
-
tokens_reset_ts = now + _parse_header_duration(tokens_reset_str)
|
|
163
|
-
tokens_info = (tokens_limit, tokens_remaining, tokens_reset_ts)
|
|
164
|
-
|
|
165
|
-
return requests_info, tokens_info
|
|
178
|
+
return super().get_retry_delay(exc)
|
|
166
179
|
|
|
167
180
|
|
|
168
181
|
#####################################
|
|
@@ -170,7 +183,7 @@ def _get_header_info(
|
|
|
170
183
|
|
|
171
184
|
|
|
172
185
|
@pxt.udf
|
|
173
|
-
async def speech(input: str, *, model: str, voice: str, model_kwargs:
|
|
186
|
+
async def speech(input: str, *, model: str, voice: str, model_kwargs: dict[str, Any] | None = None) -> pxt.Audio:
|
|
174
187
|
"""
|
|
175
188
|
Generates audio from the input text.
|
|
176
189
|
|
|
@@ -205,20 +218,15 @@ async def speech(input: str, *, model: str, voice: str, model_kwargs: Optional[d
|
|
|
205
218
|
if model_kwargs is None:
|
|
206
219
|
model_kwargs = {}
|
|
207
220
|
|
|
208
|
-
content = await _openai_client().audio.speech.create(
|
|
209
|
-
input=input,
|
|
210
|
-
model=model,
|
|
211
|
-
voice=voice, # type: ignore
|
|
212
|
-
**model_kwargs,
|
|
213
|
-
)
|
|
221
|
+
content = await _openai_client().audio.speech.create(input=input, model=model, voice=voice, **model_kwargs)
|
|
214
222
|
ext = model_kwargs.get('response_format', 'mp3')
|
|
215
|
-
output_filename = str(
|
|
223
|
+
output_filename = str(TempStore.create_path(extension=f'.{ext}'))
|
|
216
224
|
content.write_to_file(output_filename)
|
|
217
225
|
return output_filename
|
|
218
226
|
|
|
219
227
|
|
|
220
228
|
@pxt.udf
|
|
221
|
-
async def transcriptions(audio: pxt.Audio, *, model: str, model_kwargs:
|
|
229
|
+
async def transcriptions(audio: pxt.Audio, *, model: str, model_kwargs: dict[str, Any] | None = None) -> dict:
|
|
222
230
|
"""
|
|
223
231
|
Transcribes audio into the input language.
|
|
224
232
|
|
|
@@ -257,7 +265,7 @@ async def transcriptions(audio: pxt.Audio, *, model: str, model_kwargs: Optional
|
|
|
257
265
|
|
|
258
266
|
|
|
259
267
|
@pxt.udf
|
|
260
|
-
async def translations(audio: pxt.Audio, *, model: str, model_kwargs:
|
|
268
|
+
async def translations(audio: pxt.Audio, *, model: str, model_kwargs: dict[str, Any] | None = None) -> dict:
|
|
261
269
|
"""
|
|
262
270
|
Translates audio into English.
|
|
263
271
|
|
|
@@ -327,7 +335,7 @@ def _is_model_family(model: str, family: str) -> bool:
|
|
|
327
335
|
|
|
328
336
|
|
|
329
337
|
def _chat_completions_get_request_resources(
|
|
330
|
-
messages: list, model: str, model_kwargs:
|
|
338
|
+
messages: list, model: str, model_kwargs: dict[str, Any] | None
|
|
331
339
|
) -> dict[str, int]:
|
|
332
340
|
if model_kwargs is None:
|
|
333
341
|
model_kwargs = {}
|
|
@@ -354,9 +362,10 @@ async def chat_completions(
|
|
|
354
362
|
messages: list,
|
|
355
363
|
*,
|
|
356
364
|
model: str,
|
|
357
|
-
model_kwargs:
|
|
358
|
-
tools:
|
|
359
|
-
tool_choice:
|
|
365
|
+
model_kwargs: dict[str, Any] | None = None,
|
|
366
|
+
tools: list[dict[str, Any]] | None = None,
|
|
367
|
+
tool_choice: dict[str, Any] | None = None,
|
|
368
|
+
_runtime_ctx: env.RuntimeCtx | None = None,
|
|
360
369
|
) -> dict:
|
|
361
370
|
"""
|
|
362
371
|
Creates a model response for the given chat conversation.
|
|
@@ -386,10 +395,10 @@ async def chat_completions(
|
|
|
386
395
|
of the table `tbl`:
|
|
387
396
|
|
|
388
397
|
>>> messages = [
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
398
|
+
... {'role': 'system', 'content': 'You are a helpful assistant.'},
|
|
399
|
+
... {'role': 'user', 'content': tbl.prompt}
|
|
400
|
+
... ]
|
|
401
|
+
>>> tbl.add_computed_column(response=chat_completions(messages, model='gpt-4o-mini'))
|
|
393
402
|
"""
|
|
394
403
|
if model_kwargs is None:
|
|
395
404
|
model_kwargs = {}
|
|
@@ -420,13 +429,14 @@ async def chat_completions(
|
|
|
420
429
|
)
|
|
421
430
|
|
|
422
431
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
423
|
-
|
|
432
|
+
is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
|
|
433
|
+
rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
|
|
424
434
|
|
|
425
435
|
return json.loads(result.text)
|
|
426
436
|
|
|
427
437
|
|
|
428
438
|
def _vision_get_request_resources(
|
|
429
|
-
prompt: str, image: PIL.Image.Image, model: str, model_kwargs:
|
|
439
|
+
prompt: str, image: PIL.Image.Image, model: str, model_kwargs: dict[str, Any] | None = None
|
|
430
440
|
) -> dict[str, int]:
|
|
431
441
|
if model_kwargs is None:
|
|
432
442
|
model_kwargs = {}
|
|
@@ -463,7 +473,12 @@ def _vision_get_request_resources(
|
|
|
463
473
|
|
|
464
474
|
@pxt.udf
|
|
465
475
|
async def vision(
|
|
466
|
-
prompt: str,
|
|
476
|
+
prompt: str,
|
|
477
|
+
image: PIL.Image.Image,
|
|
478
|
+
*,
|
|
479
|
+
model: str,
|
|
480
|
+
model_kwargs: dict[str, Any] | None = None,
|
|
481
|
+
_runtime_ctx: env.RuntimeCtx | None = None,
|
|
467
482
|
) -> str:
|
|
468
483
|
"""
|
|
469
484
|
Analyzes an image with the OpenAI vision capability. This is a convenience function that takes an image and
|
|
@@ -523,8 +538,10 @@ async def vision(
|
|
|
523
538
|
**model_kwargs,
|
|
524
539
|
)
|
|
525
540
|
|
|
541
|
+
# _logger.debug(f'vision(): headers={result.headers}')
|
|
526
542
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
527
|
-
|
|
543
|
+
is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
|
|
544
|
+
rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
|
|
528
545
|
|
|
529
546
|
result = json.loads(result.text)
|
|
530
547
|
return result['choices'][0]['message']['content']
|
|
@@ -547,7 +564,11 @@ def _embeddings_get_request_resources(input: list[str]) -> dict[str, int]:
|
|
|
547
564
|
|
|
548
565
|
@pxt.udf(batch_size=32)
|
|
549
566
|
async def embeddings(
|
|
550
|
-
input: Batch[str],
|
|
567
|
+
input: Batch[str],
|
|
568
|
+
*,
|
|
569
|
+
model: str,
|
|
570
|
+
model_kwargs: dict[str, Any] | None = None,
|
|
571
|
+
_runtime_ctx: env.RuntimeCtx | None = None,
|
|
551
572
|
) -> Batch[pxt.Array[(None,), pxt.Float]]:
|
|
552
573
|
"""
|
|
553
574
|
Creates an embedding vector representing the input text.
|
|
@@ -594,13 +615,14 @@ async def embeddings(
|
|
|
594
615
|
input=input, model=model, encoding_format='float', **model_kwargs
|
|
595
616
|
)
|
|
596
617
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
597
|
-
|
|
618
|
+
is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
|
|
619
|
+
rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
|
|
598
620
|
return [np.array(data['embedding'], dtype=np.float64) for data in json.loads(result.content)['data']]
|
|
599
621
|
|
|
600
622
|
|
|
601
623
|
@embeddings.conditional_return_type
|
|
602
|
-
def _(model: str, model_kwargs:
|
|
603
|
-
dimensions:
|
|
624
|
+
def _(model: str, model_kwargs: dict[str, Any] | None = None) -> ts.ArrayType:
|
|
625
|
+
dimensions: int | None = None
|
|
604
626
|
if model_kwargs is not None:
|
|
605
627
|
dimensions = model_kwargs.get('dimensions')
|
|
606
628
|
if dimensions is None:
|
|
@@ -617,7 +639,7 @@ def _(model: str, model_kwargs: Optional[dict[str, Any]] = None) -> ts.ArrayType
|
|
|
617
639
|
|
|
618
640
|
@pxt.udf
|
|
619
641
|
async def image_generations(
|
|
620
|
-
prompt: str, *, model: str = 'dall-e-2', model_kwargs:
|
|
642
|
+
prompt: str, *, model: str = 'dall-e-2', model_kwargs: dict[str, Any] | None = None
|
|
621
643
|
) -> PIL.Image.Image:
|
|
622
644
|
"""
|
|
623
645
|
Creates an image given a prompt.
|
|
@@ -663,7 +685,7 @@ async def image_generations(
|
|
|
663
685
|
|
|
664
686
|
|
|
665
687
|
@image_generations.conditional_return_type
|
|
666
|
-
def _(model_kwargs:
|
|
688
|
+
def _(model_kwargs: dict[str, Any] | None = None) -> ts.ImageType:
|
|
667
689
|
if model_kwargs is None or 'size' not in model_kwargs:
|
|
668
690
|
# default size is 1024x1024
|
|
669
691
|
return ts.ImageType(size=(1024, 1024))
|
|
@@ -739,7 +761,7 @@ def invoke_tools(tools: Tools, response: exprs.Expr) -> exprs.InlineDict:
|
|
|
739
761
|
|
|
740
762
|
|
|
741
763
|
@pxt.udf
|
|
742
|
-
def _openai_response_to_pxt_tool_calls(response: dict) ->
|
|
764
|
+
def _openai_response_to_pxt_tool_calls(response: dict) -> dict | None:
|
|
743
765
|
if 'tool_calls' not in response['choices'][0]['message'] or response['choices'][0]['message']['tool_calls'] is None:
|
|
744
766
|
return None
|
|
745
767
|
openai_tool_calls = response['choices'][0]['message']['tool_calls']
|