pixeltable 0.4.6__py3-none-any.whl → 0.4.7__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 +4 -2
- pixeltable/catalog/__init__.py +1 -1
- pixeltable/catalog/catalog.py +3 -3
- pixeltable/catalog/column.py +49 -0
- pixeltable/catalog/insertable_table.py +0 -7
- pixeltable/catalog/schema_object.py +1 -14
- pixeltable/catalog/table.py +139 -53
- pixeltable/catalog/table_version.py +30 -138
- pixeltable/catalog/view.py +2 -1
- pixeltable/dataframe.py +2 -3
- pixeltable/env.py +43 -5
- pixeltable/exec/expr_eval/expr_eval_node.py +2 -2
- pixeltable/exec/expr_eval/schedulers.py +36 -15
- pixeltable/exprs/array_slice.py +2 -2
- pixeltable/exprs/data_row.py +13 -0
- pixeltable/exprs/expr.py +9 -9
- pixeltable/exprs/function_call.py +2 -2
- pixeltable/exprs/globals.py +1 -2
- pixeltable/exprs/json_path.py +3 -3
- pixeltable/exprs/row_builder.py +14 -16
- pixeltable/exprs/string_op.py +3 -3
- pixeltable/func/query_template_function.py +2 -2
- pixeltable/func/signature.py +30 -3
- pixeltable/func/tools.py +2 -2
- pixeltable/functions/anthropic.py +75 -25
- pixeltable/functions/globals.py +2 -2
- pixeltable/functions/llama_cpp.py +9 -1
- pixeltable/functions/openai.py +74 -54
- pixeltable/functions/video.py +54 -1
- pixeltable/functions/vision.py +2 -2
- pixeltable/globals.py +74 -12
- pixeltable/io/datarows.py +3 -3
- pixeltable/io/fiftyone.py +4 -4
- pixeltable/io/globals.py +3 -3
- pixeltable/io/hf_datasets.py +4 -4
- pixeltable/io/pandas.py +6 -6
- pixeltable/io/parquet.py +3 -3
- pixeltable/io/table_data_conduit.py +2 -2
- pixeltable/io/utils.py +2 -2
- pixeltable/iterators/document.py +2 -2
- pixeltable/iterators/video.py +49 -9
- pixeltable/share/packager.py +45 -36
- pixeltable/store.py +5 -25
- pixeltable/type_system.py +5 -8
- pixeltable/utils/__init__.py +2 -2
- pixeltable/utils/arrow.py +5 -5
- pixeltable/utils/description_helper.py +3 -3
- pixeltable/utils/iceberg.py +1 -2
- {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/METADATA +70 -19
- {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/RECORD +53 -53
- {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/WHEEL +0 -0
- {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/entry_points.txt +0 -0
- {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/licenses/LICENSE +0 -0
pixeltable/exprs/json_path.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, Optional
|
|
3
|
+
from typing import Any, Optional
|
|
4
4
|
|
|
5
5
|
import jmespath
|
|
6
6
|
import sqlalchemy as sql
|
|
@@ -18,7 +18,7 @@ from .sql_element_cache import SqlElementCache
|
|
|
18
18
|
|
|
19
19
|
class JsonPath(Expr):
|
|
20
20
|
def __init__(
|
|
21
|
-
self, anchor: Optional[Expr], path_elements: Optional[list[
|
|
21
|
+
self, anchor: Optional[Expr], path_elements: Optional[list[str | int | slice]] = None, scope_idx: int = 0
|
|
22
22
|
) -> None:
|
|
23
23
|
"""
|
|
24
24
|
anchor can be None, in which case this is a relative JsonPath and the anchor is set later via set_anchor().
|
|
@@ -30,7 +30,7 @@ class JsonPath(Expr):
|
|
|
30
30
|
super().__init__(ts.JsonType(nullable=True)) # JsonPath expressions are always nullable
|
|
31
31
|
if anchor is not None:
|
|
32
32
|
self.components = [anchor]
|
|
33
|
-
self.path_elements: list[
|
|
33
|
+
self.path_elements: list[str | int | slice] = path_elements
|
|
34
34
|
self.compiled_path = jmespath.compile(self._json_path()) if len(path_elements) > 0 else None
|
|
35
35
|
self.scope_idx = scope_idx
|
|
36
36
|
# NOTE: the _create_id() result will change if set_anchor() gets called;
|
pixeltable/exprs/row_builder.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import sys
|
|
4
4
|
import time
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Any, Iterable, Optional, Sequence
|
|
6
|
+
from typing import Any, Iterable, NamedTuple, Optional, Sequence
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
@@ -34,8 +34,7 @@ class ExecProfile:
|
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
class ColumnSlotIdx:
|
|
37
|
+
class ColumnSlotIdx(NamedTuple):
|
|
39
38
|
"""Info for how to locate materialized column in DataRow
|
|
40
39
|
TODO: can this be integrated into RowBuilder directly?
|
|
41
40
|
"""
|
|
@@ -127,7 +126,7 @@ class RowBuilder:
|
|
|
127
126
|
)
|
|
128
127
|
|
|
129
128
|
# if init(columns):
|
|
130
|
-
# - we are creating table rows and need to record columns for
|
|
129
|
+
# - we are creating table rows and need to record columns for create_store_table_row()
|
|
131
130
|
# - output_exprs materialize those columns
|
|
132
131
|
# - input_exprs are ColumnRefs of the non-computed columns (ie, what needs to be provided as input)
|
|
133
132
|
# - media validation:
|
|
@@ -445,20 +444,20 @@ class RowBuilder:
|
|
|
445
444
|
expr, f'expression {expr}', data_row.get_exc(expr.slot_idx), exc_tb, input_vals, 0
|
|
446
445
|
) from exc
|
|
447
446
|
|
|
448
|
-
def
|
|
447
|
+
def create_store_table_row(
|
|
449
448
|
self, data_row: DataRow, cols_with_excs: Optional[set[int]], pk: tuple[int, ...]
|
|
450
449
|
) -> tuple[list[Any], int]:
|
|
451
|
-
"""Create a table row from the slots that have an output column assigned
|
|
450
|
+
"""Create a store table row from the slots that have an output column assigned
|
|
452
451
|
|
|
453
452
|
Return tuple[list of row values in `self.table_columns` order, # of exceptions]
|
|
454
453
|
This excludes system columns.
|
|
454
|
+
Row values are converted to their store type.
|
|
455
455
|
"""
|
|
456
456
|
from pixeltable.exprs.column_property_ref import ColumnPropertyRef
|
|
457
457
|
|
|
458
458
|
num_excs = 0
|
|
459
459
|
table_row: list[Any] = list(pk)
|
|
460
|
-
for
|
|
461
|
-
col, slot_idx = info.col, info.slot_idx
|
|
460
|
+
for col, slot_idx in self.table_columns:
|
|
462
461
|
if data_row.has_exc(slot_idx):
|
|
463
462
|
exc = data_row.get_exc(slot_idx)
|
|
464
463
|
num_excs += 1
|
|
@@ -469,9 +468,11 @@ class RowBuilder:
|
|
|
469
468
|
# exceptions get stored in the errortype/-msg properties of the cellmd column
|
|
470
469
|
table_row.append(ColumnPropertyRef.create_cellmd_exc(exc))
|
|
471
470
|
else:
|
|
472
|
-
if col.col_type.
|
|
473
|
-
|
|
474
|
-
|
|
471
|
+
if col.col_type.is_media_type():
|
|
472
|
+
if col.col_type.is_image_type() and data_row.file_urls[slot_idx] is None:
|
|
473
|
+
# we have yet to store this image
|
|
474
|
+
data_row.flush_img(slot_idx, col)
|
|
475
|
+
data_row.move_tmp_media_file(slot_idx, col)
|
|
475
476
|
val = data_row.get_stored_val(slot_idx, col.get_sa_col_type())
|
|
476
477
|
table_row.append(val)
|
|
477
478
|
if col.stores_cellmd:
|
|
@@ -479,7 +480,7 @@ class RowBuilder:
|
|
|
479
480
|
|
|
480
481
|
return table_row, num_excs
|
|
481
482
|
|
|
482
|
-
def store_column_names(self) ->
|
|
483
|
+
def store_column_names(self) -> list[str]:
|
|
483
484
|
"""
|
|
484
485
|
Returns the list of store column names corresponding to the table_columns of this RowBuilder.
|
|
485
486
|
The second tuple element of the return value is a dictionary containing all media columns in the
|
|
@@ -487,16 +488,13 @@ class RowBuilder:
|
|
|
487
488
|
"""
|
|
488
489
|
assert self.tbl is not None, self.table_columns
|
|
489
490
|
store_col_names: list[str] = [pk_col.name for pk_col in self.tbl.store_tbl.pk_columns()]
|
|
490
|
-
media_cols: dict[int, catalog.Column] = {}
|
|
491
491
|
|
|
492
492
|
for col in self.table_columns:
|
|
493
|
-
if col.col.col_type.is_media_type():
|
|
494
|
-
media_cols[len(store_col_names)] = col.col
|
|
495
493
|
store_col_names.append(col.col.store_name())
|
|
496
494
|
if col.col.stores_cellmd:
|
|
497
495
|
store_col_names.append(col.col.cellmd_store_name())
|
|
498
496
|
|
|
499
|
-
return store_col_names
|
|
497
|
+
return store_col_names
|
|
500
498
|
|
|
501
499
|
def make_row(self) -> exprs.DataRow:
|
|
502
500
|
"""Creates a new DataRow with the current row_builder's configuration."""
|
pixeltable/exprs/string_op.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, Optional
|
|
3
|
+
from typing import Any, Optional
|
|
4
4
|
|
|
5
5
|
import sqlalchemy as sql
|
|
6
6
|
|
|
@@ -76,7 +76,7 @@ class StringOp(Expr):
|
|
|
76
76
|
op2_val = data_row[self._op2.slot_idx]
|
|
77
77
|
data_row[self.slot_idx] = self.eval_nullable(op1_val, op2_val)
|
|
78
78
|
|
|
79
|
-
def eval_nullable(self, op1_val:
|
|
79
|
+
def eval_nullable(self, op1_val: str | None, op2_val: int | str | None) -> str | None:
|
|
80
80
|
"""
|
|
81
81
|
Return the result of evaluating the expression on two nullable int/float operands,
|
|
82
82
|
None is interpreted as SQL NULL
|
|
@@ -85,7 +85,7 @@ class StringOp(Expr):
|
|
|
85
85
|
return None
|
|
86
86
|
return self.eval_non_null(op1_val, op2_val)
|
|
87
87
|
|
|
88
|
-
def eval_non_null(self, op1_val: str, op2_val:
|
|
88
|
+
def eval_non_null(self, op1_val: str, op2_val: int | str) -> str:
|
|
89
89
|
"""
|
|
90
90
|
Return the result of evaluating the expression on two int/float operands
|
|
91
91
|
"""
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from functools import reduce
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional,
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, overload
|
|
6
6
|
|
|
7
7
|
from pixeltable import catalog, exceptions as excs, exprs, func, type_system as ts
|
|
8
8
|
|
|
@@ -129,7 +129,7 @@ def retrieval_udf(
|
|
|
129
129
|
table: catalog.Table,
|
|
130
130
|
name: Optional[str] = None,
|
|
131
131
|
description: Optional[str] = None,
|
|
132
|
-
parameters: Optional[Iterable[
|
|
132
|
+
parameters: Optional[Iterable[str | exprs.ColumnRef]] = None,
|
|
133
133
|
limit: Optional[int] = 10,
|
|
134
134
|
) -> func.QueryTemplateFunction:
|
|
135
135
|
"""
|
pixeltable/func/signature.py
CHANGED
|
@@ -84,8 +84,28 @@ class Signature:
|
|
|
84
84
|
"""
|
|
85
85
|
|
|
86
86
|
SPECIAL_PARAM_NAMES: ClassVar[list[str]] = ['group_by', 'order_by']
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
SYSTEM_PARAM_NAMES: ClassVar[list[str]] = ['_runtime_ctx']
|
|
88
|
+
|
|
89
|
+
return_type: ts.ColumnType
|
|
90
|
+
is_batched: bool
|
|
91
|
+
parameters: dict[str, Parameter] # name -> Parameter
|
|
92
|
+
parameters_by_pos: list[Parameter] # ordered by position in the signature
|
|
93
|
+
constant_parameters: list[Parameter] # parameters that are not batched
|
|
94
|
+
batched_parameters: list[Parameter] # parameters that are batched
|
|
95
|
+
required_parameters: list[Parameter] # parameters that do not have a default value
|
|
96
|
+
|
|
97
|
+
# the names of recognized system parameters in the signature; these are excluded from self.parameters
|
|
98
|
+
system_parameters: list[str]
|
|
99
|
+
|
|
100
|
+
py_signature: inspect.Signature
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
return_type: ts.ColumnType,
|
|
105
|
+
parameters: list[Parameter],
|
|
106
|
+
is_batched: bool = False,
|
|
107
|
+
system_parameters: Optional[list[str]] = None,
|
|
108
|
+
):
|
|
89
109
|
assert isinstance(return_type, ts.ColumnType)
|
|
90
110
|
self.return_type = return_type
|
|
91
111
|
self.is_batched = is_batched
|
|
@@ -95,6 +115,7 @@ class Signature:
|
|
|
95
115
|
self.constant_parameters = [p for p in parameters if not p.is_batched]
|
|
96
116
|
self.batched_parameters = [p for p in parameters if p.is_batched]
|
|
97
117
|
self.required_parameters = [p for p in parameters if not p.has_default()]
|
|
118
|
+
self.system_parameters = system_parameters if system_parameters is not None else []
|
|
98
119
|
self.py_signature = inspect.Signature([p.to_py_param() for p in self.parameters_by_pos])
|
|
99
120
|
|
|
100
121
|
def get_return_type(self) -> ts.ColumnType:
|
|
@@ -237,6 +258,7 @@ class Signature:
|
|
|
237
258
|
type_substitutions: Optional[dict] = None,
|
|
238
259
|
is_cls_method: bool = False,
|
|
239
260
|
) -> list[Parameter]:
|
|
261
|
+
"""Ignores parameters starting with '_'."""
|
|
240
262
|
from pixeltable import exprs
|
|
241
263
|
|
|
242
264
|
assert (py_fn is None) != (py_params is None)
|
|
@@ -251,6 +273,10 @@ class Signature:
|
|
|
251
273
|
for idx, param in enumerate(py_params):
|
|
252
274
|
if is_cls_method and idx == 0:
|
|
253
275
|
continue # skip 'self' or 'cls' parameter
|
|
276
|
+
if param.name in cls.SYSTEM_PARAM_NAMES:
|
|
277
|
+
continue # skip system parameters
|
|
278
|
+
if param.name.startswith('_'):
|
|
279
|
+
raise excs.Error(f"{param.name!r}: parameters starting with '_' are reserved")
|
|
254
280
|
if param.name in cls.SPECIAL_PARAM_NAMES:
|
|
255
281
|
raise excs.Error(f'{param.name!r} is a reserved parameter name')
|
|
256
282
|
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
@@ -308,5 +334,6 @@ class Signature:
|
|
|
308
334
|
raise excs.Error('Cannot infer pixeltable return type')
|
|
309
335
|
else:
|
|
310
336
|
_, return_is_batched = cls._infer_type(sig.return_annotation)
|
|
337
|
+
system_params = [param_name for param_name in sig.parameters if param_name in cls.SYSTEM_PARAM_NAMES]
|
|
311
338
|
|
|
312
|
-
return Signature(return_type, parameters, return_is_batched)
|
|
339
|
+
return Signature(return_type, parameters, return_is_batched, system_parameters=system_params)
|
pixeltable/func/tools.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar
|
|
3
3
|
|
|
4
4
|
import pydantic
|
|
5
5
|
|
|
@@ -100,7 +100,7 @@ class Tools(pydantic.BaseModel):
|
|
|
100
100
|
self,
|
|
101
101
|
auto: bool = False,
|
|
102
102
|
required: bool = False,
|
|
103
|
-
tool:
|
|
103
|
+
tool: str | Function | None = None,
|
|
104
104
|
parallel_tool_calls: bool = True,
|
|
105
105
|
) -> ToolChoice:
|
|
106
106
|
if sum([auto, required, tool is not None]) != 1:
|
|
@@ -38,6 +38,53 @@ def _anthropic_client() -> 'anthropic.AsyncAnthropic':
|
|
|
38
38
|
return env.Env.get().get_client('anthropic')
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def _get_header_info(
|
|
42
|
+
headers: httpx.Headers,
|
|
43
|
+
) -> tuple[
|
|
44
|
+
Optional[tuple[int, int, datetime.datetime]],
|
|
45
|
+
Optional[tuple[int, int, datetime.datetime]],
|
|
46
|
+
Optional[tuple[int, int, datetime.datetime]],
|
|
47
|
+
]:
|
|
48
|
+
"""Extract rate limit info from Anthropic API response headers."""
|
|
49
|
+
requests_limit_str = headers.get('anthropic-ratelimit-requests-limit')
|
|
50
|
+
requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
|
|
51
|
+
requests_remaining_str = headers.get('anthropic-ratelimit-requests-remaining')
|
|
52
|
+
requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
|
|
53
|
+
requests_reset_str = headers.get('anthropic-ratelimit-requests-reset')
|
|
54
|
+
requests_reset = (
|
|
55
|
+
datetime.datetime.fromisoformat(requests_reset_str.replace('Z', '+00:00')) if requests_reset_str else None
|
|
56
|
+
)
|
|
57
|
+
requests_info = (requests_limit, requests_remaining, requests_reset) if requests_reset else None
|
|
58
|
+
|
|
59
|
+
input_tokens_limit_str = headers.get('anthropic-ratelimit-input-tokens-limit')
|
|
60
|
+
input_tokens_limit = int(input_tokens_limit_str) if input_tokens_limit_str is not None else None
|
|
61
|
+
input_tokens_remaining_str = headers.get('anthropic-ratelimit-input-tokens-remaining')
|
|
62
|
+
input_tokens_remaining = int(input_tokens_remaining_str) if input_tokens_remaining_str is not None else None
|
|
63
|
+
input_tokens_reset_str = headers.get('anthropic-ratelimit-input-tokens-reset')
|
|
64
|
+
input_tokens_reset = (
|
|
65
|
+
datetime.datetime.fromisoformat(input_tokens_reset_str.replace('Z', '+00:00'))
|
|
66
|
+
if input_tokens_reset_str
|
|
67
|
+
else None
|
|
68
|
+
)
|
|
69
|
+
input_tokens_info = (input_tokens_limit, input_tokens_remaining, input_tokens_reset) if input_tokens_reset else None
|
|
70
|
+
|
|
71
|
+
output_tokens_limit_str = headers.get('anthropic-ratelimit-output-tokens-limit')
|
|
72
|
+
output_tokens_limit = int(output_tokens_limit_str) if output_tokens_limit_str is not None else None
|
|
73
|
+
output_tokens_remaining_str = headers.get('anthropic-ratelimit-output-tokens-remaining')
|
|
74
|
+
output_tokens_remaining = int(output_tokens_remaining_str) if output_tokens_remaining_str is not None else None
|
|
75
|
+
output_tokens_reset_str = headers.get('anthropic-ratelimit-output-tokens-reset')
|
|
76
|
+
output_tokens_reset = (
|
|
77
|
+
datetime.datetime.fromisoformat(output_tokens_reset_str.replace('Z', '+00:00'))
|
|
78
|
+
if output_tokens_reset_str
|
|
79
|
+
else None
|
|
80
|
+
)
|
|
81
|
+
output_tokens_info = (
|
|
82
|
+
(output_tokens_limit, output_tokens_remaining, output_tokens_reset) if output_tokens_reset else None
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return requests_info, input_tokens_info, output_tokens_info
|
|
86
|
+
|
|
87
|
+
|
|
41
88
|
class AnthropicRateLimitsInfo(env.RateLimitsInfo):
|
|
42
89
|
def __init__(self) -> None:
|
|
43
90
|
super().__init__(self._get_request_resources)
|
|
@@ -51,6 +98,27 @@ class AnthropicRateLimitsInfo(env.RateLimitsInfo):
|
|
|
51
98
|
input_len += len(message['content'])
|
|
52
99
|
return {'requests': 1, 'input_tokens': int(input_len / 4), 'output_tokens': max_tokens}
|
|
53
100
|
|
|
101
|
+
def record_exc(self, exc: Exception) -> None:
|
|
102
|
+
import anthropic
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
not isinstance(exc, anthropic.APIError)
|
|
106
|
+
or not hasattr(exc, 'response')
|
|
107
|
+
or not hasattr(exc.response, 'headers')
|
|
108
|
+
):
|
|
109
|
+
return
|
|
110
|
+
requests_info, input_tokens_info, output_tokens_info = _get_header_info(exc.response.headers)
|
|
111
|
+
_logger.debug(
|
|
112
|
+
f'record_exc(): requests_info={requests_info} input_tokens_info={input_tokens_info} '
|
|
113
|
+
f'output_tokens_info={output_tokens_info}'
|
|
114
|
+
)
|
|
115
|
+
self.record(requests=requests_info, input_tokens=input_tokens_info, output_tokens=output_tokens_info)
|
|
116
|
+
self.has_exc = True
|
|
117
|
+
|
|
118
|
+
retry_after_str = exc.response.headers.get('retry-after')
|
|
119
|
+
if retry_after_str is not None:
|
|
120
|
+
_logger.debug(f'retry-after: {retry_after_str}')
|
|
121
|
+
|
|
54
122
|
def get_retry_delay(self, exc: Exception) -> Optional[float]:
|
|
55
123
|
import anthropic
|
|
56
124
|
|
|
@@ -77,6 +145,7 @@ async def messages(
|
|
|
77
145
|
model_kwargs: Optional[dict[str, Any]] = None,
|
|
78
146
|
tools: Optional[list[dict[str, Any]]] = None,
|
|
79
147
|
tool_choice: Optional[dict[str, Any]] = None,
|
|
148
|
+
_runtime_ctx: Optional[env.RuntimeCtx] = None,
|
|
80
149
|
) -> dict:
|
|
81
150
|
"""
|
|
82
151
|
Create a Message.
|
|
@@ -151,32 +220,13 @@ async def messages(
|
|
|
151
220
|
messages=cast(Iterable[MessageParam], messages), model=model, max_tokens=max_tokens, **model_kwargs
|
|
152
221
|
)
|
|
153
222
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
requests_reset = datetime.datetime.fromisoformat(requests_reset_str.replace('Z', '+00:00'))
|
|
160
|
-
input_tokens_limit_str = result.headers.get('anthropic-ratelimit-input-tokens-limit')
|
|
161
|
-
input_tokens_limit = int(input_tokens_limit_str) if input_tokens_limit_str is not None else None
|
|
162
|
-
input_tokens_remaining_str = result.headers.get('anthropic-ratelimit-input-tokens-remaining')
|
|
163
|
-
input_tokens_remaining = int(input_tokens_remaining_str) if input_tokens_remaining_str is not None else None
|
|
164
|
-
input_tokens_reset_str = result.headers.get('anthropic-ratelimit-input-tokens-reset')
|
|
165
|
-
input_tokens_reset = datetime.datetime.fromisoformat(input_tokens_reset_str.replace('Z', '+00:00'))
|
|
166
|
-
output_tokens_limit_str = result.headers.get('anthropic-ratelimit-output-tokens-limit')
|
|
167
|
-
output_tokens_limit = int(output_tokens_limit_str) if output_tokens_limit_str is not None else None
|
|
168
|
-
output_tokens_remaining_str = result.headers.get('anthropic-ratelimit-output-tokens-remaining')
|
|
169
|
-
output_tokens_remaining = int(output_tokens_remaining_str) if output_tokens_remaining_str is not None else None
|
|
170
|
-
output_tokens_reset_str = result.headers.get('anthropic-ratelimit-output-tokens-reset')
|
|
171
|
-
output_tokens_reset = datetime.datetime.fromisoformat(output_tokens_reset_str.replace('Z', '+00:00'))
|
|
172
|
-
retry_after_str = result.headers.get('retry-after')
|
|
173
|
-
if retry_after_str is not None:
|
|
174
|
-
_logger.debug(f'retry-after: {retry_after_str}')
|
|
175
|
-
|
|
223
|
+
requests_info, input_tokens_info, output_tokens_info = _get_header_info(result.headers)
|
|
224
|
+
# retry_after_str = result.headers.get('retry-after')
|
|
225
|
+
# if retry_after_str is not None:
|
|
226
|
+
# _logger.debug(f'retry-after: {retry_after_str}')
|
|
227
|
+
is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
|
|
176
228
|
rate_limits_info.record(
|
|
177
|
-
requests=
|
|
178
|
-
input_tokens=(input_tokens_limit, input_tokens_remaining, input_tokens_reset),
|
|
179
|
-
output_tokens=(output_tokens_limit, output_tokens_remaining, output_tokens_reset),
|
|
229
|
+
requests=requests_info, input_tokens=input_tokens_info, output_tokens=output_tokens_info, reset_exc=is_retry
|
|
180
230
|
)
|
|
181
231
|
|
|
182
232
|
result_dict = json.loads(result.text)
|
pixeltable/functions/globals.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import builtins
|
|
2
2
|
import typing
|
|
3
|
-
from typing import Any, Callable, Optional
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
4
|
|
|
5
5
|
import sqlalchemy as sql
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ from typing import _GenericAlias # type: ignore[attr-defined] # isort: skip
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
# TODO: remove and replace calls with astype()
|
|
14
|
-
def cast(expr: exprs.Expr, target_type:
|
|
14
|
+
def cast(expr: exprs.Expr, target_type: ts.ColumnType | type | _GenericAlias) -> exprs.Expr:
|
|
15
15
|
expr.col_type = ts.ColumnType.normalize_type(target_type)
|
|
16
16
|
return expr
|
|
17
17
|
|
|
@@ -93,10 +93,18 @@ def _lookup_pretrained_model(repo_id: str, filename: Optional[str], n_gpu_layers
|
|
|
93
93
|
return _model_cache[key]
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
_model_cache: dict[tuple[str, str, int],
|
|
96
|
+
_model_cache: dict[tuple[str, str, int], 'llama_cpp.Llama'] = {}
|
|
97
97
|
_IS_GPU_AVAILABLE: Optional[bool] = None
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def cleanup() -> None:
|
|
101
|
+
for model in _model_cache.values():
|
|
102
|
+
if model._sampler is not None:
|
|
103
|
+
model._sampler.close()
|
|
104
|
+
model.close()
|
|
105
|
+
_model_cache.clear()
|
|
106
|
+
|
|
107
|
+
|
|
100
108
|
__all__ = local_public_names(__name__)
|
|
101
109
|
|
|
102
110
|
|
pixeltable/functions/openai.py
CHANGED
|
@@ -91,6 +91,49 @@ def _rate_limits_pool(model: str) -> str:
|
|
|
91
91
|
return f'rate-limits:openai:{model}'
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
# RE pattern for duration in '*-reset' headers;
|
|
95
|
+
# examples: 1d2h3ms, 4m5.6s; # fractional seconds can be reported as 0.5s or 500ms
|
|
96
|
+
_header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d+)m)?(?:([\d.]+)s)?')
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_header_duration(duration_str: str) -> datetime.timedelta:
|
|
100
|
+
match = _header_duration_pattern.match(duration_str)
|
|
101
|
+
if not match:
|
|
102
|
+
raise ValueError(f'Invalid duration format: {duration_str}')
|
|
103
|
+
|
|
104
|
+
days = int(match.group(1) or 0)
|
|
105
|
+
hours = int(match.group(2) or 0)
|
|
106
|
+
milliseconds = int(match.group(3) or 0)
|
|
107
|
+
minutes = int(match.group(4) or 0)
|
|
108
|
+
seconds = float(match.group(5) or 0)
|
|
109
|
+
|
|
110
|
+
return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _get_header_info(
|
|
114
|
+
headers: httpx.Headers,
|
|
115
|
+
) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
|
|
116
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
117
|
+
|
|
118
|
+
requests_limit_str = headers.get('x-ratelimit-limit-requests')
|
|
119
|
+
requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
|
|
120
|
+
requests_remaining_str = headers.get('x-ratelimit-remaining-requests')
|
|
121
|
+
requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
|
|
122
|
+
requests_reset_str = headers.get('x-ratelimit-reset-requests', '5s') # Default to 5 seconds
|
|
123
|
+
requests_reset_ts = now + _parse_header_duration(requests_reset_str)
|
|
124
|
+
requests_info = (requests_limit, requests_remaining, requests_reset_ts)
|
|
125
|
+
|
|
126
|
+
tokens_limit_str = headers.get('x-ratelimit-limit-tokens')
|
|
127
|
+
tokens_limit = int(tokens_limit_str) if tokens_limit_str is not None else None
|
|
128
|
+
tokens_remaining_str = headers.get('x-ratelimit-remaining-tokens')
|
|
129
|
+
tokens_remaining = int(tokens_remaining_str) if tokens_remaining_str is not None else None
|
|
130
|
+
tokens_reset_str = headers.get('x-ratelimit-reset-tokens', '5s') # Default to 5 seconds
|
|
131
|
+
tokens_reset_ts = now + _parse_header_duration(tokens_reset_str)
|
|
132
|
+
tokens_info = (tokens_limit, tokens_remaining, tokens_reset_ts)
|
|
133
|
+
|
|
134
|
+
return requests_info, tokens_info
|
|
135
|
+
|
|
136
|
+
|
|
94
137
|
class OpenAIRateLimitsInfo(env.RateLimitsInfo):
|
|
95
138
|
retryable_errors: tuple[Type[Exception], ...]
|
|
96
139
|
|
|
@@ -111,61 +154,24 @@ class OpenAIRateLimitsInfo(env.RateLimitsInfo):
|
|
|
111
154
|
openai.InternalServerError,
|
|
112
155
|
)
|
|
113
156
|
|
|
157
|
+
def record_exc(self, exc: Exception) -> None:
|
|
158
|
+
import openai
|
|
159
|
+
|
|
160
|
+
_ = isinstance(exc, openai.APIError)
|
|
161
|
+
if not isinstance(exc, openai.APIError) or not hasattr(exc, 'response') or not hasattr(exc.response, 'headers'):
|
|
162
|
+
return
|
|
163
|
+
requests_info, tokens_info = _get_header_info(exc.response.headers)
|
|
164
|
+
_logger.debug(f'record_exc(): requests_info={requests_info} tokens_info={tokens_info}')
|
|
165
|
+
self.record(requests=requests_info, tokens=tokens_info)
|
|
166
|
+
self.has_exc = True
|
|
167
|
+
|
|
114
168
|
def get_retry_delay(self, exc: Exception) -> Optional[float]:
|
|
115
169
|
import openai
|
|
116
170
|
|
|
117
171
|
if not isinstance(exc, self.retryable_errors):
|
|
118
172
|
return None
|
|
119
173
|
assert isinstance(exc, openai.APIError)
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# RE pattern for duration in '*-reset' headers;
|
|
124
|
-
# examples: 1d2h3ms, 4m5.6s; # fractional seconds can be reported as 0.5s or 500ms
|
|
125
|
-
_header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d+)m)?(?:([\d.]+)s)?')
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _parse_header_duration(duration_str: str) -> datetime.timedelta:
|
|
129
|
-
match = _header_duration_pattern.match(duration_str)
|
|
130
|
-
if not match:
|
|
131
|
-
raise ValueError(f'Invalid duration format: {duration_str}')
|
|
132
|
-
|
|
133
|
-
days = int(match.group(1) or 0)
|
|
134
|
-
hours = int(match.group(2) or 0)
|
|
135
|
-
milliseconds = int(match.group(3) or 0)
|
|
136
|
-
minutes = int(match.group(4) or 0)
|
|
137
|
-
seconds = float(match.group(5) or 0)
|
|
138
|
-
|
|
139
|
-
return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def _get_header_info(
|
|
143
|
-
headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
|
|
144
|
-
) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
|
|
145
|
-
assert requests or tokens
|
|
146
|
-
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
147
|
-
|
|
148
|
-
requests_info: Optional[tuple[int, int, datetime.datetime]] = None
|
|
149
|
-
if requests:
|
|
150
|
-
requests_limit_str = headers.get('x-ratelimit-limit-requests')
|
|
151
|
-
requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
|
|
152
|
-
requests_remaining_str = headers.get('x-ratelimit-remaining-requests')
|
|
153
|
-
requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
|
|
154
|
-
requests_reset_str = headers.get('x-ratelimit-reset-requests', '5s') # Default to 5 seconds
|
|
155
|
-
requests_reset_ts = now + _parse_header_duration(requests_reset_str)
|
|
156
|
-
requests_info = (requests_limit, requests_remaining, requests_reset_ts)
|
|
157
|
-
|
|
158
|
-
tokens_info: Optional[tuple[int, int, datetime.datetime]] = None
|
|
159
|
-
if tokens:
|
|
160
|
-
tokens_limit_str = headers.get('x-ratelimit-limit-tokens')
|
|
161
|
-
tokens_limit = int(tokens_limit_str) if tokens_limit_str is not None else None
|
|
162
|
-
tokens_remaining_str = headers.get('x-ratelimit-remaining-tokens')
|
|
163
|
-
tokens_remaining = int(tokens_remaining_str) if tokens_remaining_str is not None else None
|
|
164
|
-
tokens_reset_str = headers.get('x-ratelimit-reset-tokens', '5s') # Default to 5 seconds
|
|
165
|
-
tokens_reset_ts = now + _parse_header_duration(tokens_reset_str)
|
|
166
|
-
tokens_info = (tokens_limit, tokens_remaining, tokens_reset_ts)
|
|
167
|
-
|
|
168
|
-
return requests_info, tokens_info
|
|
174
|
+
return super().get_retry_delay(exc)
|
|
169
175
|
|
|
170
176
|
|
|
171
177
|
#####################################
|
|
@@ -355,6 +361,7 @@ async def chat_completions(
|
|
|
355
361
|
model_kwargs: Optional[dict[str, Any]] = None,
|
|
356
362
|
tools: Optional[list[dict[str, Any]]] = None,
|
|
357
363
|
tool_choice: Optional[dict[str, Any]] = None,
|
|
364
|
+
_runtime_ctx: Optional[env.RuntimeCtx] = None,
|
|
358
365
|
) -> dict:
|
|
359
366
|
"""
|
|
360
367
|
Creates a model response for the given chat conversation.
|
|
@@ -418,7 +425,8 @@ async def chat_completions(
|
|
|
418
425
|
)
|
|
419
426
|
|
|
420
427
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
421
|
-
|
|
428
|
+
is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
|
|
429
|
+
rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
|
|
422
430
|
|
|
423
431
|
return json.loads(result.text)
|
|
424
432
|
|
|
@@ -461,7 +469,12 @@ def _vision_get_request_resources(
|
|
|
461
469
|
|
|
462
470
|
@pxt.udf
|
|
463
471
|
async def vision(
|
|
464
|
-
prompt: str,
|
|
472
|
+
prompt: str,
|
|
473
|
+
image: PIL.Image.Image,
|
|
474
|
+
*,
|
|
475
|
+
model: str,
|
|
476
|
+
model_kwargs: Optional[dict[str, Any]] = None,
|
|
477
|
+
_runtime_ctx: Optional[env.RuntimeCtx] = None,
|
|
465
478
|
) -> str:
|
|
466
479
|
"""
|
|
467
480
|
Analyzes an image with the OpenAI vision capability. This is a convenience function that takes an image and
|
|
@@ -521,8 +534,10 @@ async def vision(
|
|
|
521
534
|
**model_kwargs,
|
|
522
535
|
)
|
|
523
536
|
|
|
537
|
+
# _logger.debug(f'vision(): headers={result.headers}')
|
|
524
538
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
525
|
-
|
|
539
|
+
is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
|
|
540
|
+
rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
|
|
526
541
|
|
|
527
542
|
result = json.loads(result.text)
|
|
528
543
|
return result['choices'][0]['message']['content']
|
|
@@ -545,7 +560,11 @@ def _embeddings_get_request_resources(input: list[str]) -> dict[str, int]:
|
|
|
545
560
|
|
|
546
561
|
@pxt.udf(batch_size=32)
|
|
547
562
|
async def embeddings(
|
|
548
|
-
input: Batch[str],
|
|
563
|
+
input: Batch[str],
|
|
564
|
+
*,
|
|
565
|
+
model: str,
|
|
566
|
+
model_kwargs: Optional[dict[str, Any]] = None,
|
|
567
|
+
_runtime_ctx: Optional[env.RuntimeCtx] = None,
|
|
549
568
|
) -> Batch[pxt.Array[(None,), pxt.Float]]:
|
|
550
569
|
"""
|
|
551
570
|
Creates an embedding vector representing the input text.
|
|
@@ -592,7 +611,8 @@ async def embeddings(
|
|
|
592
611
|
input=input, model=model, encoding_format='float', **model_kwargs
|
|
593
612
|
)
|
|
594
613
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
595
|
-
|
|
614
|
+
is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
|
|
615
|
+
rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
|
|
596
616
|
return [np.array(data['embedding'], dtype=np.float64) for data in json.loads(result.content)['data']]
|
|
597
617
|
|
|
598
618
|
|
pixeltable/functions/video.py
CHANGED
|
@@ -12,7 +12,7 @@ import pixeltable as pxt
|
|
|
12
12
|
from pixeltable import env
|
|
13
13
|
from pixeltable.utils.code import local_public_names
|
|
14
14
|
|
|
15
|
-
_format_defaults = { # format -> (codec, ext)
|
|
15
|
+
_format_defaults: dict[str, tuple[str, str]] = { # format -> (codec, ext)
|
|
16
16
|
'wav': ('pcm_s16le', 'wav'),
|
|
17
17
|
'mp3': ('libmp3lame', 'mp3'),
|
|
18
18
|
'flac': ('flac', 'flac'),
|
|
@@ -40,6 +40,59 @@ _format_defaults = { # format -> (codec, ext)
|
|
|
40
40
|
class make_video(pxt.Aggregator):
|
|
41
41
|
"""
|
|
42
42
|
Aggregator that creates a video from a sequence of images.
|
|
43
|
+
|
|
44
|
+
Creates an H.264 encoded MP4 video from a sequence of PIL Image frames. This aggregator requires the input
|
|
45
|
+
frames to be ordered (typically by frame position) and is commonly used with `FrameIterator` views to
|
|
46
|
+
reconstruct videos from processed frames.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
fps: Frames per second for the output video. Default is 25. This is set when the aggregator is created.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
|
|
53
|
+
- A `pxt.Video` containing the created video file path.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
Create a video from frames extracted using FrameIterator:
|
|
57
|
+
|
|
58
|
+
>>> import pixeltable as pxt
|
|
59
|
+
>>> from pixeltable.functions.video import make_video
|
|
60
|
+
>>> from pixeltable.iterators import FrameIterator
|
|
61
|
+
>>>
|
|
62
|
+
>>> # Create base table for videos
|
|
63
|
+
>>> videos_table = pxt.create_table('videos', {'video': pxt.Video})
|
|
64
|
+
>>>
|
|
65
|
+
>>> # Create view to extract frames
|
|
66
|
+
>>> frames_view = pxt.create_view(
|
|
67
|
+
... 'video_frames',
|
|
68
|
+
... videos_table,
|
|
69
|
+
... iterator=FrameIterator.create(video=videos_table.video, fps=1)
|
|
70
|
+
... )
|
|
71
|
+
>>>
|
|
72
|
+
>>> # Reconstruct video from frames
|
|
73
|
+
>>> frames_view.group_by(videos_table).select(
|
|
74
|
+
... make_video(frames_view.pos, frames_view.frame)
|
|
75
|
+
... ).show()
|
|
76
|
+
|
|
77
|
+
Apply transformations to frames before creating a video:
|
|
78
|
+
|
|
79
|
+
>>> # Add computed column with transformed frames
|
|
80
|
+
>>> frames_view.add_computed_column(
|
|
81
|
+
... rotated_frame=frames_view.frame.rotate(30),
|
|
82
|
+
... stored=True
|
|
83
|
+
... )
|
|
84
|
+
>>>
|
|
85
|
+
>>> # Create video from transformed frames
|
|
86
|
+
>>> frames_view.group_by(videos_table).select(
|
|
87
|
+
... make_video(frames_view.pos, frames_view.rotated_frame)
|
|
88
|
+
... ).show()
|
|
89
|
+
|
|
90
|
+
Compare multiple processed versions side-by-side:
|
|
91
|
+
|
|
92
|
+
>>> frames_view.group_by(videos_table).select(
|
|
93
|
+
... make_video(frames_view.pos, frames_view.frame),
|
|
94
|
+
... make_video(frames_view.pos, frames_view.rotated_frame)
|
|
95
|
+
... ).show()
|
|
43
96
|
"""
|
|
44
97
|
|
|
45
98
|
container: Optional[av.container.OutputContainer]
|