pixeltable 0.2.28__py3-none-any.whl → 0.2.30__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 (62) hide show
  1. pixeltable/__init__.py +1 -1
  2. pixeltable/__version__.py +2 -2
  3. pixeltable/catalog/__init__.py +1 -1
  4. pixeltable/catalog/dir.py +6 -0
  5. pixeltable/catalog/globals.py +25 -0
  6. pixeltable/catalog/named_function.py +4 -0
  7. pixeltable/catalog/path_dict.py +37 -11
  8. pixeltable/catalog/schema_object.py +6 -0
  9. pixeltable/catalog/table.py +96 -19
  10. pixeltable/catalog/table_version.py +22 -8
  11. pixeltable/dataframe.py +201 -3
  12. pixeltable/env.py +9 -3
  13. pixeltable/exec/expr_eval_node.py +1 -1
  14. pixeltable/exec/sql_node.py +2 -2
  15. pixeltable/exprs/function_call.py +134 -29
  16. pixeltable/exprs/inline_expr.py +22 -2
  17. pixeltable/exprs/row_builder.py +1 -1
  18. pixeltable/exprs/similarity_expr.py +9 -2
  19. pixeltable/func/__init__.py +1 -0
  20. pixeltable/func/aggregate_function.py +151 -68
  21. pixeltable/func/callable_function.py +50 -16
  22. pixeltable/func/expr_template_function.py +62 -24
  23. pixeltable/func/function.py +191 -23
  24. pixeltable/func/function_registry.py +2 -1
  25. pixeltable/func/query_template_function.py +11 -6
  26. pixeltable/func/signature.py +64 -7
  27. pixeltable/func/tools.py +116 -0
  28. pixeltable/func/udf.py +57 -35
  29. pixeltable/functions/__init__.py +2 -2
  30. pixeltable/functions/anthropic.py +36 -2
  31. pixeltable/functions/globals.py +54 -34
  32. pixeltable/functions/json.py +3 -8
  33. pixeltable/functions/math.py +67 -0
  34. pixeltable/functions/ollama.py +4 -4
  35. pixeltable/functions/openai.py +31 -2
  36. pixeltable/functions/timestamp.py +1 -1
  37. pixeltable/functions/video.py +2 -8
  38. pixeltable/functions/vision.py +1 -1
  39. pixeltable/globals.py +347 -79
  40. pixeltable/index/embedding_index.py +44 -24
  41. pixeltable/metadata/__init__.py +1 -1
  42. pixeltable/metadata/converters/convert_16.py +2 -1
  43. pixeltable/metadata/converters/convert_17.py +2 -1
  44. pixeltable/metadata/converters/convert_23.py +35 -0
  45. pixeltable/metadata/converters/convert_24.py +47 -0
  46. pixeltable/metadata/converters/util.py +4 -2
  47. pixeltable/metadata/notes.py +2 -0
  48. pixeltable/metadata/schema.py +1 -0
  49. pixeltable/type_system.py +192 -48
  50. {pixeltable-0.2.28.dist-info → pixeltable-0.2.30.dist-info}/METADATA +4 -2
  51. {pixeltable-0.2.28.dist-info → pixeltable-0.2.30.dist-info}/RECORD +54 -57
  52. pixeltable-0.2.30.dist-info/entry_points.txt +3 -0
  53. pixeltable/tool/create_test_db_dump.py +0 -311
  54. pixeltable/tool/create_test_video.py +0 -81
  55. pixeltable/tool/doc_plugins/griffe.py +0 -50
  56. pixeltable/tool/doc_plugins/mkdocstrings.py +0 -6
  57. pixeltable/tool/doc_plugins/templates/material/udf.html.jinja +0 -135
  58. pixeltable/tool/embed_udf.py +0 -9
  59. pixeltable/tool/mypy_plugin.py +0 -55
  60. pixeltable-0.2.28.dist-info/entry_points.txt +0 -3
  61. {pixeltable-0.2.28.dist-info → pixeltable-0.2.30.dist-info}/LICENSE +0 -0
  62. {pixeltable-0.2.28.dist-info → pixeltable-0.2.30.dist-info}/WHEEL +0 -0
pixeltable/func/udf.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Callable, Optional, overload
3
+ from typing import Any, Callable, Optional, Sequence, overload
4
4
 
5
5
  import pixeltable.exceptions as excs
6
6
  import pixeltable.type_system as ts
7
7
 
8
8
  from .callable_function import CallableFunction
9
- from .expr_template_function import ExprTemplateFunction
9
+ from .expr_template_function import ExprTemplateFunction, ExprTemplate
10
10
  from .function import Function
11
11
  from .function_registry import FunctionRegistry
12
12
  from .globals import validate_symbol_path
@@ -21,13 +21,14 @@ def udf(decorated_fn: Callable) -> Function: ...
21
21
  # Decorator schema invoked with parentheses: @pxt.udf(**kwargs)
22
22
  @overload
23
23
  def udf(
24
- *,
25
- batch_size: Optional[int] = None,
26
- substitute_fn: Optional[Callable] = None,
27
- is_method: bool = False,
28
- is_property: bool = False,
29
- _force_stored: bool = False
30
- ) -> Callable[[Callable], Function]: ...
24
+ *,
25
+ batch_size: Optional[int] = None,
26
+ substitute_fn: Optional[Callable] = None,
27
+ is_method: bool = False,
28
+ is_property: bool = False,
29
+ type_substitutions: Optional[Sequence[dict]] = None,
30
+ _force_stored: bool = False
31
+ ) -> Callable[[Callable], CallableFunction]: ...
31
32
 
32
33
 
33
34
  def udf(*args, **kwargs):
@@ -52,6 +53,7 @@ def udf(*args, **kwargs):
52
53
  substitute_fn = kwargs.pop('substitute_fn', None)
53
54
  is_method = kwargs.pop('is_method', None)
54
55
  is_property = kwargs.pop('is_property', None)
56
+ type_substitutions = kwargs.pop('type_substitutions', None)
55
57
  force_stored = kwargs.pop('_force_stored', False)
56
58
  if len(kwargs) > 0:
57
59
  raise excs.Error(f'Invalid @udf decorator kwargs: {", ".join(kwargs.keys())}')
@@ -65,6 +67,7 @@ def udf(*args, **kwargs):
65
67
  substitute_fn=substitute_fn,
66
68
  is_method=is_method,
67
69
  is_property=is_property,
70
+ type_substitutions=type_substitutions,
68
71
  force_stored=force_stored
69
72
  )
70
73
 
@@ -79,9 +82,10 @@ def make_function(
79
82
  substitute_fn: Optional[Callable] = None,
80
83
  is_method: bool = False,
81
84
  is_property: bool = False,
85
+ type_substitutions: Optional[Sequence[dict]] = None,
82
86
  function_name: Optional[str] = None,
83
87
  force_stored: bool = False
84
- ) -> Function:
88
+ ) -> CallableFunction:
85
89
  """
86
90
  Constructs a `CallableFunction` from the specified parameters.
87
91
  If `substitute_fn` is specified, then `decorated_fn`
@@ -104,25 +108,43 @@ def make_function(
104
108
  # Display name to use for error messages
105
109
  errmsg_name = function_name if function_path is None else function_path
106
110
 
107
- sig = Signature.create(decorated_fn, param_types, return_type)
108
-
109
- # batched functions must have a batched return type
110
- # TODO: remove 'Python' from the error messages when we have full inference with Annotated types
111
- if batch_size is not None and not sig.is_batched:
112
- raise excs.Error(f'{errmsg_name}(): batch_size is specified; Python return type must be a `Batch`')
113
- if batch_size is not None and len(sig.batched_parameters) == 0:
114
- raise excs.Error(f'{errmsg_name}(): batch_size is specified; at least one Python parameter must be `Batch`')
115
- if batch_size is None and len(sig.batched_parameters) > 0:
116
- raise excs.Error(f'{errmsg_name}(): batched parameters in udf, but no `batch_size` given')
117
-
118
- if is_method and is_property:
119
- raise excs.Error(f'Cannot specify both `is_method` and `is_property` (in function `{function_name}`)')
120
- if is_property and len(sig.parameters) != 1:
121
- raise excs.Error(
122
- f"`is_property=True` expects a UDF with exactly 1 parameter, but `{function_name}` has {len(sig.parameters)}"
123
- )
124
- if (is_method or is_property) and function_path is None:
125
- raise excs.Error('Stored functions cannot be declared using `is_method` or `is_property`')
111
+ signatures: list[Signature]
112
+ if type_substitutions is None:
113
+ sig = Signature.create(decorated_fn, param_types, return_type)
114
+
115
+ # batched functions must have a batched return type
116
+ # TODO: remove 'Python' from the error messages when we have full inference with Annotated types
117
+ if batch_size is not None and not sig.is_batched:
118
+ raise excs.Error(f'{errmsg_name}(): batch_size is specified; Python return type must be a `Batch`')
119
+ if batch_size is not None and len(sig.batched_parameters) == 0:
120
+ raise excs.Error(f'{errmsg_name}(): batch_size is specified; at least one Python parameter must be `Batch`')
121
+ if batch_size is None and len(sig.batched_parameters) > 0:
122
+ raise excs.Error(f'{errmsg_name}(): batched parameters in udf, but no `batch_size` given')
123
+
124
+ if is_method and is_property:
125
+ raise excs.Error(f'Cannot specify both `is_method` and `is_property` (in function `{function_name}`)')
126
+ if is_property and len(sig.parameters) != 1:
127
+ raise excs.Error(
128
+ f"`is_property=True` expects a UDF with exactly 1 parameter, but `{function_name}` has {len(sig.parameters)}"
129
+ )
130
+ if (is_method or is_property) and function_path is None:
131
+ raise excs.Error('Stored functions cannot be declared using `is_method` or `is_property`')
132
+
133
+ signatures = [sig]
134
+ else:
135
+ if function_path is None:
136
+ raise excs.Error(
137
+ f'{errmsg_name}(): type substitutions can only be used with module UDFs (not locally defined UDFs)'
138
+ )
139
+ if batch_size is not None:
140
+ raise excs.Error(f'{errmsg_name}(): type substitutions cannot be used with batched functions')
141
+ if is_method is not None or is_property is not None:
142
+ # TODO: Support this for `is_method`?
143
+ raise excs.Error(f'{errmsg_name}(): type substitutions cannot be used with `is_method` or `is_property`')
144
+ signatures = [
145
+ Signature.create(decorated_fn, param_types, return_type, type_substitutions=subst)
146
+ for subst in type_substitutions
147
+ ]
126
148
 
127
149
  if substitute_fn is None:
128
150
  py_fn = decorated_fn
@@ -132,8 +154,8 @@ def make_function(
132
154
  py_fn = substitute_fn
133
155
 
134
156
  result = CallableFunction(
135
- signature=sig,
136
- py_fn=py_fn,
157
+ signatures=signatures,
158
+ py_fns=[py_fn] * len(signatures), # All signatures share the same Python function
137
159
  self_path=function_path,
138
160
  self_name=function_name,
139
161
  batch_size=batch_size,
@@ -171,12 +193,12 @@ def expr_udf(*args: Any, **kwargs: Any) -> Any:
171
193
  import pixeltable.exprs as exprs
172
194
  var_exprs = [exprs.Variable(param.name, param.col_type) for param in sig.parameters.values()]
173
195
  # call the function with the parameter expressions to construct an Expr with parameters
174
- template = py_fn(*var_exprs)
175
- assert isinstance(template, exprs.Expr)
176
- sig.return_type = template.col_type
196
+ expr = py_fn(*var_exprs)
197
+ assert isinstance(expr, exprs.Expr)
198
+ sig.return_type = expr.col_type
177
199
  if function_path is not None:
178
200
  validate_symbol_path(function_path)
179
- return ExprTemplateFunction(template, sig, self_path=function_path, name=py_fn.__name__)
201
+ return ExprTemplateFunction([ExprTemplate(expr, sig)], self_path=function_path, name=py_fn.__name__)
180
202
 
181
203
  if len(args) == 1:
182
204
  assert len(kwargs) == 0 and callable(args[0])
@@ -1,7 +1,7 @@
1
1
  from pixeltable.utils.code import local_public_names
2
2
 
3
- from . import (anthropic, audio, fireworks, gemini, huggingface, image, json, llama_cpp, mistralai, ollama, openai,
4
- string, timestamp, together, video, vision, whisper)
3
+ from . import (anthropic, audio, fireworks, gemini, huggingface, image, json, llama_cpp, math, mistralai, ollama,
4
+ openai, string, timestamp, together, video, vision, whisper)
5
5
  from .globals import *
6
6
 
7
7
  __all__ = local_public_names(__name__, exclude=['globals']) + local_public_names(globals.__name__)
@@ -10,7 +10,8 @@ from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
10
10
  import tenacity
11
11
 
12
12
  import pixeltable as pxt
13
- from pixeltable import env
13
+ from pixeltable import env, exprs
14
+ from pixeltable.func import Tools
14
15
  from pixeltable.utils.code import local_public_names
15
16
 
16
17
  if TYPE_CHECKING:
@@ -47,7 +48,7 @@ def messages(
47
48
  system: Optional[str] = None,
48
49
  temperature: Optional[float] = None,
49
50
  tool_choice: Optional[list[dict]] = None,
50
- tools: Optional[dict] = None,
51
+ tools: Optional[list[dict]] = None,
51
52
  top_k: Optional[int] = None,
52
53
  top_p: Optional[float] = None,
53
54
  ) -> dict:
@@ -77,6 +78,21 @@ def messages(
77
78
  >>> msgs = [{'role': 'user', 'content': tbl.prompt}]
78
79
  ... tbl['response'] = messages(msgs, model='claude-3-haiku-20240307')
79
80
  """
81
+ if tools is not None:
82
+ # Reformat `tools` into Anthropic format
83
+ tools = [
84
+ {
85
+ 'name': tool['name'],
86
+ 'description': tool['description'],
87
+ 'input_schema': {
88
+ 'type': 'object',
89
+ 'properties': tool['parameters']['properties'],
90
+ 'required': tool['required'],
91
+ },
92
+ }
93
+ for tool in tools
94
+ ]
95
+
80
96
  return _retry(_anthropic_client().messages.create)(
81
97
  messages=messages,
82
98
  model=model,
@@ -92,6 +108,24 @@ def messages(
92
108
  ).dict()
93
109
 
94
110
 
111
+ def invoke_tools(tools: Tools, response: exprs.Expr) -> exprs.InlineDict:
112
+ """Converts an Anthropic response dict to Pixeltable tool invocation format and calls `tools._invoke()`."""
113
+ return tools._invoke(_anthropic_response_to_pxt_tool_calls(response))
114
+
115
+
116
+ @pxt.udf
117
+ def _anthropic_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
118
+ anthropic_tool_calls = [r for r in response['content'] if r['type'] == 'tool_use']
119
+ if len(anthropic_tool_calls) > 0:
120
+ return {
121
+ tool_call['name']: {
122
+ 'args': tool_call['input']
123
+ }
124
+ for tool_call in anthropic_tool_calls
125
+ }
126
+ return None
127
+
128
+
95
129
  _T = TypeVar('_T')
96
130
 
97
131
 
@@ -1,6 +1,7 @@
1
1
  import builtins
2
2
  from typing import _GenericAlias # type: ignore[attr-defined]
3
3
  from typing import Optional, Union
4
+ import typing
4
5
 
5
6
  import sqlalchemy as sql
6
7
 
@@ -16,23 +17,24 @@ def cast(expr: exprs.Expr, target_type: Union[ts.ColumnType, type, _GenericAlias
16
17
  return expr
17
18
 
18
19
 
19
- @func.uda(
20
- update_types=[ts.IntType(nullable=True)], value_type=ts.IntType(nullable=False),
21
- allows_window=True, requires_order_by=False)
22
- class sum(func.Aggregator):
20
+ T = typing.TypeVar('T')
21
+
22
+
23
+ @func.uda(allows_window=True, type_substitutions=({T: Optional[int]}, {T: Optional[float]})) # type: ignore[misc]
24
+ class sum(func.Aggregator, typing.Generic[T]):
23
25
  """Sums the selected integers or floats."""
24
26
  def __init__(self):
25
- self.sum: Optional[int] = None
27
+ self.sum: T = None
26
28
 
27
- def update(self, val: Optional[int]) -> None:
29
+ def update(self, val: T) -> None:
28
30
  if val is None:
29
31
  return
30
32
  if self.sum is None:
31
33
  self.sum = val
32
34
  else:
33
- self.sum += val
35
+ self.sum += val # type: ignore[operator]
34
36
 
35
- def value(self) -> Union[int, float]:
37
+ def value(self) -> T:
36
38
  return self.sum
37
39
 
38
40
 
@@ -43,12 +45,22 @@ def _(val: sql.ColumnElement) -> Optional[sql.ColumnElement]:
43
45
  return sql.sql.func.sum(val)
44
46
 
45
47
 
46
- @func.uda(update_types=[ts.IntType(nullable=True)], value_type=ts.IntType(), allows_window=True, requires_order_by=False)
47
- class count(func.Aggregator):
48
+ @func.uda(
49
+ allows_window=True,
50
+ # Allow counting non-null values of any type
51
+ # TODO: I couldn't include "Array" because we don't have a way to represent a generic array (of arbitrary dimension).
52
+ # TODO: should we have an "Any" type that can be used here?
53
+ type_substitutions=tuple(
54
+ {T: Optional[t]} # type: ignore[misc]
55
+ for t in (ts.String, ts.Int, ts.Float, ts.Bool, ts.Timestamp,
56
+ ts.Json, ts.Image, ts.Video, ts.Audio, ts.Document)
57
+ ),
58
+ )
59
+ class count(func.Aggregator, typing.Generic[T]):
48
60
  def __init__(self):
49
61
  self.count = 0
50
62
 
51
- def update(self, val: Optional[int]) -> None:
63
+ def update(self, val: T) -> None:
52
64
  if val is not None:
53
65
  self.count += 1
54
66
 
@@ -62,74 +74,82 @@ def _(val: sql.ColumnElement) -> Optional[sql.ColumnElement]:
62
74
 
63
75
 
64
76
  @func.uda(
65
- update_types=[ts.IntType(nullable=True)], value_type=ts.IntType(nullable=True), allows_window=True,
66
- requires_order_by=False)
67
- class min(func.Aggregator):
77
+ allows_window=True,
78
+ type_substitutions=tuple({T: Optional[t]} for t in (str, int, float, bool, ts.Timestamp)) # type: ignore[misc]
79
+ )
80
+ class min(func.Aggregator, typing.Generic[T]):
68
81
  def __init__(self):
69
- self.val: Optional[int] = None
82
+ self.val: T = None
70
83
 
71
- def update(self, val: Optional[int]) -> None:
84
+ def update(self, val: T) -> None:
72
85
  if val is None:
73
86
  return
74
87
  if self.val is None:
75
88
  self.val = val
76
89
  else:
77
- self.val = builtins.min(self.val, val)
90
+ self.val = builtins.min(self.val, val) # type: ignore[call-overload]
78
91
 
79
- def value(self) -> Optional[int]:
92
+ def value(self) -> T:
80
93
  return self.val
81
94
 
82
95
 
83
96
  @min.to_sql
84
97
  def _(val: sql.ColumnElement) -> Optional[sql.ColumnElement]:
98
+ if val.type.python_type == bool:
99
+ # TODO: min/max aggregation of booleans is not supported in Postgres (but it is in Python).
100
+ # Right now we simply force the computation to be done in Python; we might consider implementing an alternate
101
+ # way of doing it in SQL. (min/max of booleans is simply logical and/or, respectively.)
102
+ return None
85
103
  return sql.sql.func.min(val)
86
104
 
87
105
 
88
106
  @func.uda(
89
- update_types=[ts.IntType(nullable=True)], value_type=ts.IntType(nullable=True), allows_window=True,
90
- requires_order_by=False)
91
- class max(func.Aggregator):
107
+ allows_window=True,
108
+ type_substitutions=tuple({T: Optional[t]} for t in (str, int, float, bool, ts.Timestamp)) # type: ignore[misc]
109
+ )
110
+ class max(func.Aggregator, typing.Generic[T]):
92
111
  def __init__(self):
93
- self.val: Optional[int] = None
112
+ self.val: T = None
94
113
 
95
- def update(self, val: Optional[int]) -> None:
114
+ def update(self, val: T) -> None:
96
115
  if val is None:
97
116
  return
98
117
  if self.val is None:
99
118
  self.val = val
100
119
  else:
101
- self.val = builtins.max(self.val, val)
120
+ self.val = builtins.max(self.val, val) # type: ignore[call-overload]
102
121
 
103
- def value(self) -> Optional[int]:
122
+ def value(self) -> T:
104
123
  return self.val
105
124
 
106
125
 
107
126
  @max.to_sql
108
127
  def _(val: sql.ColumnElement) -> Optional[sql.ColumnElement]:
128
+ if val.type.python_type == bool:
129
+ # TODO: see comment in @min.to_sql.
130
+ return None
109
131
  return sql.sql.func.max(val)
110
132
 
111
133
 
112
- @func.uda(
113
- update_types=[ts.IntType(nullable=True)], value_type=ts.FloatType(nullable=True), allows_window=False,
114
- requires_order_by=False)
115
- class mean(func.Aggregator):
134
+ @func.uda(type_substitutions=({T: Optional[int]}, {T: Optional[float]})) # type: ignore[misc]
135
+ class mean(func.Aggregator, typing.Generic[T]):
116
136
  def __init__(self):
117
- self.sum: Optional[int] = None
137
+ self.sum: T = None
118
138
  self.count = 0
119
139
 
120
- def update(self, val: Optional[int]) -> None:
140
+ def update(self, val: T) -> None:
121
141
  if val is None:
122
142
  return
123
143
  if self.sum is None:
124
144
  self.sum = val
125
145
  else:
126
- self.sum += val
146
+ self.sum += val # type: ignore[operator]
127
147
  self.count += 1
128
148
 
129
- def value(self) -> Optional[float]:
149
+ def value(self) -> Optional[float]: # Always a float
130
150
  if self.count == 0:
131
151
  return None
132
- return self.sum / self.count
152
+ return self.sum / self.count # type: ignore[operator]
133
153
 
134
154
 
135
155
  @mean.to_sql
@@ -16,20 +16,15 @@ import pixeltable as pxt
16
16
  from pixeltable.utils.code import local_public_names
17
17
 
18
18
 
19
- @pxt.uda(
20
- update_types=[pxt.JsonType(nullable=True)],
21
- value_type=pxt.JsonType(),
22
- requires_order_by=False,
23
- allows_window=False,
24
- )
19
+ @pxt.uda
25
20
  class make_list(pxt.Aggregator):
26
21
  """
27
22
  Collects arguments into a list.
28
23
  """
29
- def __init__(self):
24
+ def __init__(self) -> None:
30
25
  self.output: list[Any] = []
31
26
 
32
- def update(self, obj: Any) -> None:
27
+ def update(self, obj: pxt.Json) -> None:
33
28
  if obj is None:
34
29
  return
35
30
  self.output.append(obj)
@@ -0,0 +1,67 @@
1
+ import builtins
2
+ import math
3
+ from typing import Optional
4
+
5
+ import sqlalchemy as sql
6
+
7
+ import pixeltable as pxt
8
+ from pixeltable.utils.code import local_public_names
9
+
10
+
11
+ @pxt.udf(is_method=True)
12
+ def abs(self: float) -> float:
13
+ return builtins.abs(self)
14
+
15
+
16
+ @abs.to_sql
17
+ def _(self: sql.ColumnElement) -> sql.ColumnElement:
18
+ return sql.func.abs(self)
19
+
20
+
21
+ @pxt.udf(is_method=True)
22
+ def ceil(self: float) -> float:
23
+ # This ensures the same behavior as SQL
24
+ if math.isfinite(self):
25
+ return float(math.ceil(self))
26
+ else:
27
+ return self
28
+
29
+
30
+ @ceil.to_sql
31
+ def _(self: sql.ColumnElement) -> sql.ColumnElement:
32
+ return sql.func.ceiling(self)
33
+
34
+
35
+ @pxt.udf(is_method=True)
36
+ def floor(self: float) -> float:
37
+ # This ensures the same behavior as SQL
38
+ if math.isfinite(self):
39
+ return float(math.floor(self))
40
+ else:
41
+ return self
42
+
43
+
44
+ @floor.to_sql
45
+ def _(self: sql.ColumnElement) -> sql.ColumnElement:
46
+ return sql.func.floor(self)
47
+
48
+
49
+ @pxt.udf(is_method=True)
50
+ def round(self: float, digits: Optional[int] = None) -> float:
51
+ # Set digits explicitly to 0 to guarantee a return type of float; this ensures the same behavior as SQL
52
+ return builtins.round(self, digits or 0)
53
+
54
+
55
+ @round.to_sql
56
+ def _(self: sql.ColumnElement, digits: Optional[sql.ColumnElement] = None) -> sql.ColumnElement:
57
+ if digits is None:
58
+ return sql.func.round(self)
59
+ else:
60
+ return sql.func.round(sql.cast(self, sql.Numeric), sql.cast(digits, sql.Integer))
61
+
62
+
63
+ __all__ = local_public_names(__name__)
64
+
65
+
66
+ def __dir__():
67
+ return __all__
@@ -34,7 +34,7 @@ def generate(
34
34
  template: str = '',
35
35
  context: Optional[list[int]] = None,
36
36
  raw: bool = False,
37
- format: str = '',
37
+ format: Optional[str] = None,
38
38
  options: Optional[dict] = None,
39
39
  ) -> dict:
40
40
  """
@@ -44,7 +44,7 @@ def generate(
44
44
  prompt: The prompt to generate a response for.
45
45
  model: The model name.
46
46
  suffix: The text after the model response.
47
- format: The format of the response; must be one of `'json'` or `''` (the empty string).
47
+ format: The format of the response; must be one of `'json'` or `None`.
48
48
  system: System message.
49
49
  template: Prompt template to use.
50
50
  context: The context parameter returned from a previous call to `generate()`.
@@ -77,7 +77,7 @@ def chat(
77
77
  *,
78
78
  model: str,
79
79
  tools: Optional[list[dict]] = None,
80
- format: str = '',
80
+ format: Optional[str] = None,
81
81
  options: Optional[dict] = None,
82
82
  ) -> dict:
83
83
  """
@@ -87,7 +87,7 @@ def chat(
87
87
  messages: The messages of the chat.
88
88
  model: The model name.
89
89
  tools: Tools for the model to use.
90
- format: The format of the response; must be one of `'json'` or `''` (the empty string).
90
+ format: The format of the response; must be one of `'json'` or `None`.
91
91
  options: Additional options to pass to the `chat` call, such as `max_tokens`, `temperature`, `top_p`, and `top_k`.
92
92
  For details, see the
93
93
  [Valid Parameters and Values](https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values)
@@ -7,6 +7,7 @@ the [Working with OpenAI](https://pixeltable.readme.io/docs/working-with-openai)
7
7
 
8
8
  import base64
9
9
  import io
10
+ import json
10
11
  import pathlib
11
12
  import uuid
12
13
  from typing import TYPE_CHECKING, Callable, Optional, TypeVar, Union
@@ -16,8 +17,8 @@ import PIL.Image
16
17
  import tenacity
17
18
 
18
19
  import pixeltable as pxt
19
- from pixeltable import env
20
- from pixeltable.func import Batch
20
+ from pixeltable import env, exprs
21
+ from pixeltable.func import Batch, Tools
21
22
  from pixeltable.utils.code import local_public_names
22
23
 
23
24
  if TYPE_CHECKING:
@@ -225,6 +226,16 @@ def chat_completions(
225
226
  ]
226
227
  tbl['response'] = chat_completions(messages, model='gpt-4o-mini')
227
228
  """
229
+
230
+ if tools is not None:
231
+ tools = [
232
+ {
233
+ 'type': 'function',
234
+ 'function': tool
235
+ }
236
+ for tool in tools
237
+ ]
238
+
228
239
  result = _retry(_openai_client().chat.completions.create)(
229
240
  messages=messages,
230
241
  model=model,
@@ -453,6 +464,24 @@ def moderations(input: str, *, model: Optional[str] = None) -> dict:
453
464
  return result.dict()
454
465
 
455
466
 
467
+ def invoke_tools(tools: Tools, response: exprs.Expr) -> exprs.InlineDict:
468
+ """Converts an OpenAI response dict to Pixeltable tool invocation format and calls `tools._invoke()`."""
469
+ return tools._invoke(_openai_response_to_pxt_tool_calls(response))
470
+
471
+
472
+ @pxt.udf
473
+ def _openai_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
474
+ openai_tool_calls = response['choices'][0]['message']['tool_calls']
475
+ if openai_tool_calls is not None:
476
+ return {
477
+ tool_call['function']['name']: {
478
+ 'args': json.loads(tool_call['function']['arguments'])
479
+ }
480
+ for tool_call in openai_tool_calls
481
+ }
482
+ return None
483
+
484
+
456
485
  _T = TypeVar('_T')
457
486
 
458
487
 
@@ -232,7 +232,7 @@ def _(
232
232
  sql.cast(day, sql.Integer),
233
233
  sql.cast(hour, sql.Integer),
234
234
  sql.cast(minute, sql.Integer),
235
- sql.cast(second + microsecond / 1000000.0, sql.Double))
235
+ sql.cast(second + microsecond / 1000000.0, sql.Float))
236
236
 
237
237
  # @pxt.udf
238
238
  # def date(self: datetime) -> datetime:
@@ -47,13 +47,7 @@ _format_defaults = { # format -> (codec, ext)
47
47
  # output_container.mux(packet)
48
48
 
49
49
 
50
- @pxt.uda(
51
- init_types=[pxt.IntType()],
52
- update_types=[pxt.ImageType()],
53
- value_type=pxt.VideoType(),
54
- requires_order_by=True,
55
- allows_window=False,
56
- )
50
+ @pxt.uda(requires_order_by=True)
57
51
  class make_video(pxt.Aggregator):
58
52
  """
59
53
  Aggregator that creates a video from a sequence of images.
@@ -80,7 +74,7 @@ class make_video(pxt.Aggregator):
80
74
  for packet in self.stream.encode(av_frame):
81
75
  self.container.mux(packet)
82
76
 
83
- def value(self) -> str:
77
+ def value(self) -> pxt.Video:
84
78
  for packet in self.stream.encode():
85
79
  self.container.mux(packet)
86
80
  self.container.close()
@@ -220,7 +220,7 @@ def eval_detections(
220
220
  return result
221
221
 
222
222
 
223
- @pxt.uda(update_types=[pxt.JsonType()], value_type=pxt.JsonType(), allows_std_agg=True, allows_window=False)
223
+ @pxt.uda
224
224
  class mean_ap(pxt.Aggregator):
225
225
  """
226
226
  Calculates the mean average precision (mAP) over