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.

Files changed (202) hide show
  1. pixeltable/__init__.py +23 -5
  2. pixeltable/_version.py +1 -0
  3. pixeltable/catalog/__init__.py +5 -3
  4. pixeltable/catalog/catalog.py +1318 -404
  5. pixeltable/catalog/column.py +186 -115
  6. pixeltable/catalog/dir.py +1 -2
  7. pixeltable/catalog/globals.py +11 -43
  8. pixeltable/catalog/insertable_table.py +167 -79
  9. pixeltable/catalog/path.py +61 -23
  10. pixeltable/catalog/schema_object.py +9 -10
  11. pixeltable/catalog/table.py +626 -308
  12. pixeltable/catalog/table_metadata.py +101 -0
  13. pixeltable/catalog/table_version.py +713 -569
  14. pixeltable/catalog/table_version_handle.py +37 -6
  15. pixeltable/catalog/table_version_path.py +42 -29
  16. pixeltable/catalog/tbl_ops.py +50 -0
  17. pixeltable/catalog/update_status.py +191 -0
  18. pixeltable/catalog/view.py +108 -94
  19. pixeltable/config.py +128 -22
  20. pixeltable/dataframe.py +188 -100
  21. pixeltable/env.py +407 -136
  22. pixeltable/exceptions.py +6 -0
  23. pixeltable/exec/__init__.py +3 -0
  24. pixeltable/exec/aggregation_node.py +7 -8
  25. pixeltable/exec/cache_prefetch_node.py +83 -110
  26. pixeltable/exec/cell_materialization_node.py +231 -0
  27. pixeltable/exec/cell_reconstruction_node.py +135 -0
  28. pixeltable/exec/component_iteration_node.py +4 -3
  29. pixeltable/exec/data_row_batch.py +8 -65
  30. pixeltable/exec/exec_context.py +16 -4
  31. pixeltable/exec/exec_node.py +13 -36
  32. pixeltable/exec/expr_eval/evaluators.py +7 -6
  33. pixeltable/exec/expr_eval/expr_eval_node.py +27 -12
  34. pixeltable/exec/expr_eval/globals.py +8 -5
  35. pixeltable/exec/expr_eval/row_buffer.py +1 -2
  36. pixeltable/exec/expr_eval/schedulers.py +190 -30
  37. pixeltable/exec/globals.py +32 -0
  38. pixeltable/exec/in_memory_data_node.py +18 -18
  39. pixeltable/exec/object_store_save_node.py +293 -0
  40. pixeltable/exec/row_update_node.py +16 -9
  41. pixeltable/exec/sql_node.py +206 -101
  42. pixeltable/exprs/__init__.py +1 -1
  43. pixeltable/exprs/arithmetic_expr.py +27 -22
  44. pixeltable/exprs/array_slice.py +3 -3
  45. pixeltable/exprs/column_property_ref.py +34 -30
  46. pixeltable/exprs/column_ref.py +92 -96
  47. pixeltable/exprs/comparison.py +5 -5
  48. pixeltable/exprs/compound_predicate.py +5 -4
  49. pixeltable/exprs/data_row.py +152 -55
  50. pixeltable/exprs/expr.py +62 -43
  51. pixeltable/exprs/expr_dict.py +3 -3
  52. pixeltable/exprs/expr_set.py +17 -10
  53. pixeltable/exprs/function_call.py +75 -37
  54. pixeltable/exprs/globals.py +1 -2
  55. pixeltable/exprs/in_predicate.py +4 -4
  56. pixeltable/exprs/inline_expr.py +10 -27
  57. pixeltable/exprs/is_null.py +1 -3
  58. pixeltable/exprs/json_mapper.py +8 -8
  59. pixeltable/exprs/json_path.py +56 -22
  60. pixeltable/exprs/literal.py +5 -5
  61. pixeltable/exprs/method_ref.py +2 -2
  62. pixeltable/exprs/object_ref.py +2 -2
  63. pixeltable/exprs/row_builder.py +127 -53
  64. pixeltable/exprs/rowid_ref.py +8 -12
  65. pixeltable/exprs/similarity_expr.py +50 -25
  66. pixeltable/exprs/sql_element_cache.py +4 -4
  67. pixeltable/exprs/string_op.py +5 -5
  68. pixeltable/exprs/type_cast.py +3 -5
  69. pixeltable/func/__init__.py +1 -0
  70. pixeltable/func/aggregate_function.py +8 -8
  71. pixeltable/func/callable_function.py +9 -9
  72. pixeltable/func/expr_template_function.py +10 -10
  73. pixeltable/func/function.py +18 -20
  74. pixeltable/func/function_registry.py +6 -7
  75. pixeltable/func/globals.py +2 -3
  76. pixeltable/func/mcp.py +74 -0
  77. pixeltable/func/query_template_function.py +20 -18
  78. pixeltable/func/signature.py +43 -16
  79. pixeltable/func/tools.py +23 -13
  80. pixeltable/func/udf.py +18 -20
  81. pixeltable/functions/__init__.py +6 -0
  82. pixeltable/functions/anthropic.py +93 -33
  83. pixeltable/functions/audio.py +114 -10
  84. pixeltable/functions/bedrock.py +13 -6
  85. pixeltable/functions/date.py +1 -1
  86. pixeltable/functions/deepseek.py +20 -9
  87. pixeltable/functions/fireworks.py +2 -2
  88. pixeltable/functions/gemini.py +28 -11
  89. pixeltable/functions/globals.py +13 -13
  90. pixeltable/functions/groq.py +108 -0
  91. pixeltable/functions/huggingface.py +1046 -23
  92. pixeltable/functions/image.py +9 -18
  93. pixeltable/functions/llama_cpp.py +23 -8
  94. pixeltable/functions/math.py +3 -4
  95. pixeltable/functions/mistralai.py +4 -15
  96. pixeltable/functions/ollama.py +16 -9
  97. pixeltable/functions/openai.py +104 -82
  98. pixeltable/functions/openrouter.py +143 -0
  99. pixeltable/functions/replicate.py +2 -2
  100. pixeltable/functions/reve.py +250 -0
  101. pixeltable/functions/string.py +21 -28
  102. pixeltable/functions/timestamp.py +13 -14
  103. pixeltable/functions/together.py +4 -6
  104. pixeltable/functions/twelvelabs.py +92 -0
  105. pixeltable/functions/util.py +6 -1
  106. pixeltable/functions/video.py +1388 -106
  107. pixeltable/functions/vision.py +7 -7
  108. pixeltable/functions/whisper.py +15 -7
  109. pixeltable/functions/whisperx.py +179 -0
  110. pixeltable/{ext/functions → functions}/yolox.py +2 -4
  111. pixeltable/globals.py +332 -105
  112. pixeltable/index/base.py +13 -22
  113. pixeltable/index/btree.py +23 -22
  114. pixeltable/index/embedding_index.py +32 -44
  115. pixeltable/io/__init__.py +4 -2
  116. pixeltable/io/datarows.py +7 -6
  117. pixeltable/io/external_store.py +49 -77
  118. pixeltable/io/fiftyone.py +11 -11
  119. pixeltable/io/globals.py +29 -28
  120. pixeltable/io/hf_datasets.py +17 -9
  121. pixeltable/io/label_studio.py +70 -66
  122. pixeltable/io/lancedb.py +3 -0
  123. pixeltable/io/pandas.py +12 -11
  124. pixeltable/io/parquet.py +13 -93
  125. pixeltable/io/table_data_conduit.py +71 -47
  126. pixeltable/io/utils.py +3 -3
  127. pixeltable/iterators/__init__.py +2 -1
  128. pixeltable/iterators/audio.py +21 -11
  129. pixeltable/iterators/document.py +116 -55
  130. pixeltable/iterators/image.py +5 -2
  131. pixeltable/iterators/video.py +293 -13
  132. pixeltable/metadata/__init__.py +4 -2
  133. pixeltable/metadata/converters/convert_18.py +2 -2
  134. pixeltable/metadata/converters/convert_19.py +2 -2
  135. pixeltable/metadata/converters/convert_20.py +2 -2
  136. pixeltable/metadata/converters/convert_21.py +2 -2
  137. pixeltable/metadata/converters/convert_22.py +2 -2
  138. pixeltable/metadata/converters/convert_24.py +2 -2
  139. pixeltable/metadata/converters/convert_25.py +2 -2
  140. pixeltable/metadata/converters/convert_26.py +2 -2
  141. pixeltable/metadata/converters/convert_29.py +4 -4
  142. pixeltable/metadata/converters/convert_34.py +2 -2
  143. pixeltable/metadata/converters/convert_36.py +2 -2
  144. pixeltable/metadata/converters/convert_37.py +15 -0
  145. pixeltable/metadata/converters/convert_38.py +39 -0
  146. pixeltable/metadata/converters/convert_39.py +124 -0
  147. pixeltable/metadata/converters/convert_40.py +73 -0
  148. pixeltable/metadata/converters/util.py +13 -12
  149. pixeltable/metadata/notes.py +4 -0
  150. pixeltable/metadata/schema.py +79 -42
  151. pixeltable/metadata/utils.py +74 -0
  152. pixeltable/mypy/__init__.py +3 -0
  153. pixeltable/mypy/mypy_plugin.py +123 -0
  154. pixeltable/plan.py +274 -223
  155. pixeltable/share/__init__.py +1 -1
  156. pixeltable/share/packager.py +259 -129
  157. pixeltable/share/protocol/__init__.py +34 -0
  158. pixeltable/share/protocol/common.py +170 -0
  159. pixeltable/share/protocol/operation_types.py +33 -0
  160. pixeltable/share/protocol/replica.py +109 -0
  161. pixeltable/share/publish.py +213 -57
  162. pixeltable/store.py +238 -175
  163. pixeltable/type_system.py +104 -63
  164. pixeltable/utils/__init__.py +2 -3
  165. pixeltable/utils/arrow.py +108 -13
  166. pixeltable/utils/av.py +298 -0
  167. pixeltable/utils/azure_store.py +305 -0
  168. pixeltable/utils/code.py +3 -3
  169. pixeltable/utils/console_output.py +4 -1
  170. pixeltable/utils/coroutine.py +6 -23
  171. pixeltable/utils/dbms.py +31 -5
  172. pixeltable/utils/description_helper.py +4 -5
  173. pixeltable/utils/documents.py +5 -6
  174. pixeltable/utils/exception_handler.py +7 -30
  175. pixeltable/utils/filecache.py +6 -6
  176. pixeltable/utils/formatter.py +4 -6
  177. pixeltable/utils/gcs_store.py +283 -0
  178. pixeltable/utils/http_server.py +2 -3
  179. pixeltable/utils/iceberg.py +1 -2
  180. pixeltable/utils/image.py +17 -0
  181. pixeltable/utils/lancedb.py +88 -0
  182. pixeltable/utils/local_store.py +316 -0
  183. pixeltable/utils/misc.py +5 -0
  184. pixeltable/utils/object_stores.py +528 -0
  185. pixeltable/utils/pydantic.py +60 -0
  186. pixeltable/utils/pytorch.py +5 -6
  187. pixeltable/utils/s3_store.py +392 -0
  188. pixeltable-0.4.20.dist-info/METADATA +587 -0
  189. pixeltable-0.4.20.dist-info/RECORD +218 -0
  190. {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.20.dist-info}/WHEEL +1 -1
  191. pixeltable-0.4.20.dist-info/entry_points.txt +2 -0
  192. pixeltable/__version__.py +0 -3
  193. pixeltable/ext/__init__.py +0 -17
  194. pixeltable/ext/functions/__init__.py +0 -11
  195. pixeltable/ext/functions/whisperx.py +0 -77
  196. pixeltable/utils/media_store.py +0 -77
  197. pixeltable/utils/s3.py +0 -17
  198. pixeltable/utils/sample.py +0 -25
  199. pixeltable-0.4.0rc3.dist-info/METADATA +0 -435
  200. pixeltable-0.4.0rc3.dist-info/RECORD +0 -189
  201. pixeltable-0.4.0rc3.dist-info/entry_points.txt +0 -3
  202. {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.20.dist-info/licenses}/LICENSE +0 -0
@@ -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
- import io
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: Optional[str] = None) -> PIL.Image.Image:
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: Optional[PIL.Image.Image] = None, extrema: Optional[list] = None) -> float:
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: Optional[str] = None) -> tuple[int]:
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: Optional[int] = None,
360
+ method: int | None = None,
370
361
  kmeans: int = 0,
371
- palette: Optional[int] = None,
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: Optional[tuple[int, int, int, int]] = None) -> PIL.Image.Image:
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, Optional
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: Optional[str] = None,
18
- repo_id: Optional[str] = None,
19
- repo_filename: Optional[str] = None,
20
- model_kwargs: Optional[dict[str, Any]] = None,
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: Optional[str], n_gpu_layers: int) -> 'llama_cpp.Llama':
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], Any] = {}
97
- _IS_GPU_AVAILABLE: Optional[bool] = None
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__)
@@ -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: Optional[int] = None) -> float:
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: Optional[sql.ColumnElement] = None) -> sql.ColumnElement:
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, Optional, TypeVar, Union
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.types.basemodel
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: Optional[dict[str, Any]] = None
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: Optional[dict[str, Any]] = None) -> dict:
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
 
@@ -1,4 +1,11 @@
1
- from typing import TYPE_CHECKING, Optional
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() -> Optional['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: Optional[list[int]] = None,
43
+ context: list[int] | None = None,
37
44
  raw: bool = False,
38
- format: Optional[str] = None,
39
- options: Optional[dict] = None,
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: Optional[list[dict]] = None,
81
- format: Optional[str] = None,
82
- options: Optional[dict] = None,
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: Optional[dict] = None
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.
@@ -13,8 +13,7 @@ import logging
13
13
  import math
14
14
  import pathlib
15
15
  import re
16
- import uuid
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 get_retry_delay(self, exc: Exception) -> Optional[float]:
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 1.0
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: Optional[dict[str, Any]] = None) -> pxt.Audio:
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(env.Env.get().tmp_dir / f'{uuid.uuid4()}.{ext}')
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: Optional[dict[str, Any]] = None) -> dict:
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: Optional[dict[str, Any]] = None) -> dict:
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: Optional[dict[str, Any]]
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: Optional[dict[str, Any]] = None,
358
- tools: Optional[list[dict[str, Any]]] = None,
359
- tool_choice: Optional[dict[str, Any]] = None,
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
- {'role': 'system', 'content': 'You are a helpful assistant.'},
390
- {'role': 'user', 'content': tbl.prompt}
391
- ]
392
- tbl.add_computed_column(response=chat_completions(messages, model='gpt-4o-mini'))
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
- rate_limits_info.record(requests=requests_info, tokens=tokens_info)
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: Optional[dict[str, Any]] = None
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, image: PIL.Image.Image, *, model: str, model_kwargs: Optional[dict[str, Any]] = None
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
- rate_limits_info.record(requests=requests_info, tokens=tokens_info)
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], *, model: str, model_kwargs: Optional[dict[str, Any]] = None
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
- rate_limits_info.record(requests=requests_info, tokens=tokens_info)
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: Optional[dict[str, Any]] = None) -> ts.ArrayType:
603
- dimensions: Optional[int] = None
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: Optional[dict[str, Any]] = None
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: Optional[dict[str, Any]] = None) -> ts.ImageType:
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) -> Optional[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']