pixeltable 0.3.14__py3-none-any.whl → 0.4.0__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 (79) hide show
  1. pixeltable/__init__.py +1 -1
  2. pixeltable/__version__.py +2 -2
  3. pixeltable/catalog/__init__.py +9 -1
  4. pixeltable/catalog/catalog.py +559 -134
  5. pixeltable/catalog/column.py +36 -32
  6. pixeltable/catalog/dir.py +1 -2
  7. pixeltable/catalog/globals.py +12 -0
  8. pixeltable/catalog/insertable_table.py +30 -25
  9. pixeltable/catalog/schema_object.py +9 -6
  10. pixeltable/catalog/table.py +334 -267
  11. pixeltable/catalog/table_version.py +360 -241
  12. pixeltable/catalog/table_version_handle.py +18 -2
  13. pixeltable/catalog/table_version_path.py +86 -23
  14. pixeltable/catalog/view.py +47 -23
  15. pixeltable/dataframe.py +198 -19
  16. pixeltable/env.py +6 -4
  17. pixeltable/exceptions.py +6 -0
  18. pixeltable/exec/__init__.py +1 -1
  19. pixeltable/exec/exec_node.py +2 -0
  20. pixeltable/exec/expr_eval/evaluators.py +4 -1
  21. pixeltable/exec/expr_eval/expr_eval_node.py +4 -4
  22. pixeltable/exec/in_memory_data_node.py +1 -1
  23. pixeltable/exec/sql_node.py +188 -22
  24. pixeltable/exprs/column_property_ref.py +16 -6
  25. pixeltable/exprs/column_ref.py +33 -11
  26. pixeltable/exprs/comparison.py +1 -1
  27. pixeltable/exprs/data_row.py +5 -3
  28. pixeltable/exprs/expr.py +11 -4
  29. pixeltable/exprs/literal.py +2 -0
  30. pixeltable/exprs/row_builder.py +4 -6
  31. pixeltable/exprs/rowid_ref.py +8 -0
  32. pixeltable/exprs/similarity_expr.py +1 -0
  33. pixeltable/func/__init__.py +1 -0
  34. pixeltable/func/mcp.py +74 -0
  35. pixeltable/func/query_template_function.py +5 -3
  36. pixeltable/func/tools.py +12 -2
  37. pixeltable/func/udf.py +2 -2
  38. pixeltable/functions/__init__.py +1 -0
  39. pixeltable/functions/anthropic.py +19 -45
  40. pixeltable/functions/deepseek.py +19 -38
  41. pixeltable/functions/fireworks.py +9 -18
  42. pixeltable/functions/gemini.py +165 -33
  43. pixeltable/functions/groq.py +108 -0
  44. pixeltable/functions/llama_cpp.py +6 -6
  45. pixeltable/functions/math.py +63 -0
  46. pixeltable/functions/mistralai.py +16 -53
  47. pixeltable/functions/ollama.py +1 -1
  48. pixeltable/functions/openai.py +82 -165
  49. pixeltable/functions/string.py +212 -58
  50. pixeltable/functions/together.py +22 -80
  51. pixeltable/globals.py +10 -4
  52. pixeltable/index/base.py +5 -0
  53. pixeltable/index/btree.py +5 -0
  54. pixeltable/index/embedding_index.py +5 -0
  55. pixeltable/io/external_store.py +10 -31
  56. pixeltable/io/label_studio.py +5 -5
  57. pixeltable/io/parquet.py +4 -4
  58. pixeltable/io/table_data_conduit.py +1 -32
  59. pixeltable/metadata/__init__.py +11 -2
  60. pixeltable/metadata/converters/convert_13.py +2 -2
  61. pixeltable/metadata/converters/convert_30.py +6 -11
  62. pixeltable/metadata/converters/convert_35.py +9 -0
  63. pixeltable/metadata/converters/convert_36.py +38 -0
  64. pixeltable/metadata/converters/convert_37.py +15 -0
  65. pixeltable/metadata/converters/util.py +3 -9
  66. pixeltable/metadata/notes.py +3 -0
  67. pixeltable/metadata/schema.py +13 -1
  68. pixeltable/plan.py +135 -12
  69. pixeltable/share/packager.py +321 -20
  70. pixeltable/share/publish.py +2 -2
  71. pixeltable/store.py +31 -13
  72. pixeltable/type_system.py +30 -0
  73. pixeltable/utils/dbms.py +1 -1
  74. pixeltable/utils/formatter.py +64 -42
  75. {pixeltable-0.3.14.dist-info → pixeltable-0.4.0.dist-info}/METADATA +2 -1
  76. {pixeltable-0.3.14.dist-info → pixeltable-0.4.0.dist-info}/RECORD +79 -74
  77. {pixeltable-0.3.14.dist-info → pixeltable-0.4.0.dist-info}/LICENSE +0 -0
  78. {pixeltable-0.3.14.dist-info → pixeltable-0.4.0.dist-info}/WHEEL +0 -0
  79. {pixeltable-0.3.14.dist-info → pixeltable-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -157,12 +157,14 @@ def retrieval_udf(
157
157
  """
158
158
  # Argument validation
159
159
  col_refs: list[exprs.ColumnRef]
160
+ # TODO: get rid of references to ColumnRef internals and replace instead with a public interface
161
+ col_names = table.columns()
160
162
  if parameters is None:
161
- col_refs = [table[col_name] for col_name in table.columns if not table[col_name].col.is_computed]
163
+ col_refs = [table[col_name] for col_name in col_names if not table[col_name].col.is_computed]
162
164
  else:
163
165
  for param in parameters:
164
- if isinstance(param, str) and param not in table.columns:
165
- raise excs.Error(f'The specified parameter {param!r} is not a column of the table {table._path!r}')
166
+ if isinstance(param, str) and param not in col_names:
167
+ raise excs.Error(f'The specified parameter {param!r} is not a column of the table {table._path()!r}')
166
168
  col_refs = [table[param] if isinstance(param, str) else param for param in parameters]
167
169
 
168
170
  if len(col_refs) == 0:
pixeltable/func/tools.py CHANGED
@@ -1,8 +1,9 @@
1
+ import json
1
2
  from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
2
3
 
3
4
  import pydantic
4
5
 
5
- import pixeltable.exceptions as excs
6
+ from pixeltable import exceptions as excs, type_system as ts
6
7
 
7
8
  from .function import Function
8
9
  from .signature import Parameter
@@ -69,7 +70,9 @@ class Tool(pydantic.BaseModel):
69
70
  return _extract_float_tool_arg(kwargs, param_name=param.name)
70
71
  if param.col_type.is_bool_type():
71
72
  return _extract_bool_tool_arg(kwargs, param_name=param.name)
72
- raise AssertionError()
73
+ if param.col_type.is_json_type():
74
+ return _extract_json_tool_arg(kwargs, param_name=param.name)
75
+ raise AssertionError(param.col_type)
73
76
 
74
77
 
75
78
  class ToolChoice(pydantic.BaseModel):
@@ -137,6 +140,13 @@ def _extract_bool_tool_arg(kwargs: dict[str, Any], param_name: str) -> Optional[
137
140
  return _extract_arg(bool, kwargs, param_name)
138
141
 
139
142
 
143
+ @udf
144
+ def _extract_json_tool_arg(kwargs: dict[str, Any], param_name: str) -> Optional[ts.Json]:
145
+ if param_name in kwargs:
146
+ return json.loads(kwargs[param_name])
147
+ return None
148
+
149
+
140
150
  T = TypeVar('T')
141
151
 
142
152
 
pixeltable/func/udf.py CHANGED
@@ -262,7 +262,7 @@ def from_table(
262
262
  """
263
263
  from pixeltable import exprs
264
264
 
265
- ancestors = [tbl, *tbl._base_tables]
265
+ ancestors = [tbl, *tbl._get_base_tables()]
266
266
  ancestors.reverse() # We must traverse the ancestors in order from base to derived
267
267
 
268
268
  subst: dict[exprs.Expr, exprs.Expr] = {}
@@ -297,7 +297,7 @@ def from_table(
297
297
 
298
298
  if description is None:
299
299
  # Default description is the table comment
300
- description = tbl._comment
300
+ description = tbl._get_comment()
301
301
  if len(description) == 0:
302
302
  description = f"UDF for table '{tbl._name}'"
303
303
 
@@ -10,6 +10,7 @@ from . import (
10
10
  deepseek,
11
11
  fireworks,
12
12
  gemini,
13
+ groq,
13
14
  huggingface,
14
15
  image,
15
16
  json,
@@ -8,7 +8,7 @@ the [Working with Anthropic](https://pixeltable.readme.io/docs/working-with-anth
8
8
  import datetime
9
9
  import json
10
10
  import logging
11
- from typing import TYPE_CHECKING, Any, Iterable, Optional, TypeVar, Union, cast
11
+ from typing import TYPE_CHECKING, Any, Iterable, Optional, cast
12
12
 
13
13
  import httpx
14
14
 
@@ -73,16 +73,10 @@ async def messages(
73
73
  messages: list[dict[str, str]],
74
74
  *,
75
75
  model: str,
76
- max_tokens: int = 1024,
77
- metadata: Optional[dict[str, Any]] = None,
78
- stop_sequences: Optional[list[str]] = None,
79
- system: Optional[str] = None,
80
- temperature: Optional[float] = None,
81
- tool_choice: Optional[dict] = None,
82
- tools: Optional[list[dict]] = None,
83
- top_k: Optional[int] = None,
84
- top_p: Optional[float] = None,
85
- timeout: Optional[float] = None,
76
+ max_tokens: int,
77
+ model_kwargs: Optional[dict[str, Any]] = None,
78
+ tools: Optional[list[dict[str, Any]]] = None,
79
+ tool_choice: Optional[dict[str, Any]] = None,
86
80
  ) -> dict:
87
81
  """
88
82
  Create a Message.
@@ -101,25 +95,27 @@ async def messages(
101
95
  Args:
102
96
  messages: Input messages.
103
97
  model: The model that will complete your prompt.
104
-
105
- For details on the other parameters, see: <https://docs.anthropic.com/en/api/messages>
98
+ model_kwargs: Additional keyword args for the Anthropic `messages` API.
99
+ For details on the available parameters, see: <https://docs.anthropic.com/en/api/messages>
100
+ tools: An optional list of Pixeltable tools to use for the request.
101
+ tool_choice: An optional tool choice configuration.
106
102
 
107
103
  Returns:
108
104
  A dictionary containing the response and other metadata.
109
105
 
110
106
  Examples:
111
- Add a computed column that applies the model `claude-3-haiku-20240307`
107
+ Add a computed column that applies the model `claude-3-5-sonnet-20241022`
112
108
  to an existing Pixeltable column `tbl.prompt` of the table `tbl`:
113
109
 
114
110
  >>> msgs = [{'role': 'user', 'content': tbl.prompt}]
115
- ... tbl.add_computed_column(response=messages(msgs, model='claude-3-haiku-20240307'))
111
+ ... tbl.add_computed_column(response=messages(msgs, model='claude-3-5-sonnet-20241022'))
116
112
  """
117
-
118
- # it doesn't look like count_tokens() actually exists in the current version of the library
113
+ if model_kwargs is None:
114
+ model_kwargs = {}
119
115
 
120
116
  if tools is not None:
121
117
  # Reformat `tools` into Anthropic format
122
- tools = [
118
+ model_kwargs['tools'] = [
123
119
  {
124
120
  'name': tool['name'],
125
121
  'description': tool['description'],
@@ -132,17 +128,16 @@ async def messages(
132
128
  for tool in tools
133
129
  ]
134
130
 
135
- tool_choice_: Optional[dict] = None
136
131
  if tool_choice is not None:
137
132
  if tool_choice['auto']:
138
- tool_choice_ = {'type': 'auto'}
133
+ model_kwargs['tool_choice'] = {'type': 'auto'}
139
134
  elif tool_choice['required']:
140
- tool_choice_ = {'type': 'any'}
135
+ model_kwargs['tool_choice'] = {'type': 'any'}
141
136
  else:
142
137
  assert tool_choice['tool'] is not None
143
- tool_choice_ = {'type': 'tool', 'name': tool_choice['tool']}
138
+ model_kwargs['tool_choice'] = {'type': 'tool', 'name': tool_choice['tool']}
144
139
  if not tool_choice['parallel_tool_calls']:
145
- tool_choice_['disable_parallel_tool_use'] = True
140
+ model_kwargs['tool_choice']['disable_parallel_tool_use'] = True
146
141
 
147
142
  # make sure the pool info exists prior to making the request
148
143
  resource_pool_id = f'rate-limits:anthropic:{model}'
@@ -152,20 +147,8 @@ async def messages(
152
147
  # TODO: timeouts should be set system-wide and be user-configurable
153
148
  from anthropic.types import MessageParam
154
149
 
155
- # cast(Any, ...): avoid mypy errors
156
150
  result = await _anthropic_client().messages.with_raw_response.create(
157
- messages=cast(Iterable[MessageParam], messages),
158
- model=model,
159
- max_tokens=max_tokens,
160
- metadata=_opt(cast(Any, metadata)),
161
- stop_sequences=_opt(stop_sequences),
162
- system=_opt(system),
163
- temperature=_opt(cast(Any, temperature)),
164
- tools=_opt(cast(Any, tools)),
165
- tool_choice=_opt(cast(Any, tool_choice_)),
166
- top_k=_opt(top_k),
167
- top_p=_opt(top_p),
168
- timeout=_opt(timeout),
151
+ messages=cast(Iterable[MessageParam], messages), model=model, max_tokens=max_tokens, **model_kwargs
169
152
  )
170
153
 
171
154
  requests_limit_str = result.headers.get('anthropic-ratelimit-requests-limit')
@@ -224,15 +207,6 @@ def _anthropic_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
224
207
  return pxt_tool_calls
225
208
 
226
209
 
227
- _T = TypeVar('_T')
228
-
229
-
230
- def _opt(arg: _T) -> Union[_T, 'anthropic.NotGiven']:
231
- import anthropic
232
-
233
- return arg if arg is not None else anthropic.NOT_GIVEN
234
-
235
-
236
210
  __all__ = local_public_names(__name__)
237
211
 
238
212
 
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import TYPE_CHECKING, Any, Optional, Union, cast
2
+ from typing import TYPE_CHECKING, Any, Optional
3
3
 
4
4
  import httpx
5
5
 
@@ -7,8 +7,6 @@ import pixeltable as pxt
7
7
  from pixeltable import env
8
8
  from pixeltable.utils.code import local_public_names
9
9
 
10
- from .openai import _opt
11
-
12
10
  if TYPE_CHECKING:
13
11
  import openai
14
12
 
@@ -33,17 +31,9 @@ async def chat_completions(
33
31
  messages: list,
34
32
  *,
35
33
  model: str,
36
- frequency_penalty: Optional[float] = None,
37
- logprobs: Optional[bool] = None,
38
- top_logprobs: Optional[int] = None,
39
- max_tokens: Optional[int] = None,
40
- presence_penalty: Optional[float] = None,
41
- response_format: Optional[dict] = None,
42
- stop: Optional[list[str]] = None,
43
- temperature: Optional[float] = None,
44
- tools: Optional[list[dict]] = None,
45
- tool_choice: Optional[dict] = None,
46
- top_p: Optional[float] = None,
34
+ model_kwargs: Optional[dict[str, Any]] = None,
35
+ tools: Optional[list[dict[str, Any]]] = None,
36
+ tool_choice: Optional[dict[str, Any]] = None,
47
37
  ) -> dict:
48
38
  """
49
39
  Creates a model response for the given chat conversation.
@@ -60,8 +50,10 @@ async def chat_completions(
60
50
  Args:
61
51
  messages: A list of messages to use for chat completion, as described in the Deepseek API documentation.
62
52
  model: The model to use for chat completion.
63
-
64
- For details on the other parameters, see: <https://api-docs.deepseek.com/api/create-chat-completion>
53
+ model_kwargs: Additional keyword args for the Deepseek `chat/completions` API.
54
+ For details on the available parameters, see: <https://api-docs.deepseek.com/api/create-chat-completion>
55
+ tools: An optional list of Pixeltable tools to use for the request.
56
+ tool_choice: An optional tool choice configuration.
65
57
 
66
58
  Returns:
67
59
  A dictionary containing the response and other metadata.
@@ -76,39 +68,28 @@ async def chat_completions(
76
68
  ]
77
69
  tbl.add_computed_column(response=chat_completions(messages, model='deepseek-chat'))
78
70
  """
71
+ if model_kwargs is None:
72
+ model_kwargs = {}
73
+
79
74
  if tools is not None:
80
- tools = [{'type': 'function', 'function': tool} for tool in tools]
75
+ model_kwargs['tools'] = [{'type': 'function', 'function': tool} for tool in tools]
81
76
 
82
- tool_choice_: Union[str, dict, None] = None
83
77
  if tool_choice is not None:
84
78
  if tool_choice['auto']:
85
- tool_choice_ = 'auto'
79
+ model_kwargs['tool_choice'] = 'auto'
86
80
  elif tool_choice['required']:
87
- tool_choice_ = 'required'
81
+ model_kwargs['tool_choice'] = 'required'
88
82
  else:
89
83
  assert tool_choice['tool'] is not None
90
- tool_choice_ = {'type': 'function', 'function': {'name': tool_choice['tool']}}
84
+ model_kwargs['tool_choice'] = {'type': 'function', 'function': {'name': tool_choice['tool']}}
91
85
 
92
- extra_body: Optional[dict[str, Any]] = None
93
86
  if tool_choice is not None and not tool_choice['parallel_tool_calls']:
94
- extra_body = {'parallel_tool_calls': False}
87
+ if 'extra_body' not in model_kwargs:
88
+ model_kwargs['extra_body'] = {}
89
+ model_kwargs['extra_body']['parallel_tool_calls'] = False
95
90
 
96
- # cast(Any, ...): avoid mypy errors
97
91
  result = await _deepseek_client().chat.completions.with_raw_response.create(
98
- messages=messages,
99
- model=model,
100
- frequency_penalty=_opt(frequency_penalty),
101
- logprobs=_opt(logprobs),
102
- top_logprobs=_opt(top_logprobs),
103
- max_tokens=_opt(max_tokens),
104
- presence_penalty=_opt(presence_penalty),
105
- response_format=_opt(cast(Any, response_format)),
106
- stop=_opt(stop),
107
- temperature=_opt(temperature),
108
- tools=_opt(cast(Any, tools)),
109
- tool_choice=_opt(cast(Any, tool_choice_)),
110
- top_p=_opt(top_p),
111
- extra_body=extra_body,
92
+ messages=messages, model=model, **model_kwargs
112
93
  )
113
94
 
114
95
  return json.loads(result.text)
@@ -5,7 +5,7 @@ first `pip install fireworks-ai` and configure your Fireworks AI credentials, as
5
5
  the [Working with Fireworks](https://pixeltable.readme.io/docs/working-with-fireworks) tutorial.
6
6
  """
7
7
 
8
- from typing import TYPE_CHECKING, Optional
8
+ from typing import TYPE_CHECKING, Any, Optional
9
9
 
10
10
  import pixeltable as pxt
11
11
  from pixeltable import env
@@ -29,14 +29,7 @@ def _fireworks_client() -> 'fireworks.client.Fireworks':
29
29
 
30
30
  @pxt.udf(resource_pool='request-rate:fireworks')
31
31
  async def chat_completions(
32
- messages: list[dict[str, str]],
33
- *,
34
- model: str,
35
- max_tokens: Optional[int] = None,
36
- top_k: Optional[int] = None,
37
- top_p: Optional[float] = None,
38
- temperature: Optional[float] = None,
39
- request_timeout: Optional[int] = None,
32
+ messages: list[dict[str, str]], *, model: str, model_kwargs: Optional[dict[str, Any]] = None
40
33
  ) -> dict:
41
34
  """
42
35
  Creates a model response for the given chat conversation.
@@ -55,8 +48,8 @@ async def chat_completions(
55
48
  Args:
56
49
  messages: A list of messages comprising the conversation so far.
57
50
  model: The name of the model to use.
58
-
59
- For details on the other parameters, see: <https://docs.fireworks.ai/api-reference/post-chatcompletions>
51
+ model_kwargs: Additional keyword args for the Fireworks `chat_completions` API. For details on the available
52
+ parameters, see: <https://docs.fireworks.ai/api-reference/post-chatcompletions>
60
53
 
61
54
  Returns:
62
55
  A dictionary containing the response and other metadata.
@@ -70,20 +63,18 @@ async def chat_completions(
70
63
  ... response=chat_completions(messages, model='accounts/fireworks/models/mixtral-8x22b-instruct')
71
64
  ... )
72
65
  """
73
- kwargs = {'max_tokens': max_tokens, 'top_k': top_k, 'top_p': top_p, 'temperature': temperature}
74
- kwargs_not_none = {k: v for k, v in kwargs.items() if v is not None}
66
+ if model_kwargs is None:
67
+ model_kwargs = {}
75
68
 
76
69
  # for debugging purposes:
77
70
  # res_sync = _fireworks_client().chat.completions.create(model=model, messages=messages, **kwargs_not_none)
78
71
  # res_sync_dict = res_sync.dict()
79
72
 
80
- if request_timeout is None:
81
- request_timeout = Config.get().get_int_value('timeout', section='fireworks') or 600
73
+ if 'request_timeout' not in model_kwargs:
74
+ model_kwargs['request_timeout'] = Config.get().get_int_value('timeout', section='fireworks') or 600
82
75
  # TODO: this timeout doesn't really work, I think it only applies to returning the stream, but not to the timing
83
76
  # of the chunks; addressing this would require a timeout for the task running this udf
84
- stream = _fireworks_client().chat.completions.acreate(
85
- model=model, messages=messages, request_timeout=request_timeout, **kwargs_not_none
86
- )
77
+ stream = _fireworks_client().chat.completions.acreate(model=model, messages=messages, **model_kwargs)
87
78
  chunks = []
88
79
  async for chunk in stream:
89
80
  chunks.append(chunk)
@@ -5,10 +5,16 @@ first `pip install google-genai` and configure your Gemini credentials, as descr
5
5
  the [Working with Gemini](https://pixeltable.readme.io/docs/working-with-gemini) tutorial.
6
6
  """
7
7
 
8
+ import asyncio
9
+ import io
10
+ import tempfile
11
+ from pathlib import Path
8
12
  from typing import TYPE_CHECKING, Optional
9
13
 
14
+ import PIL.Image
15
+
10
16
  import pixeltable as pxt
11
- from pixeltable import env
17
+ from pixeltable import env, exceptions as excs, exprs
12
18
 
13
19
  if TYPE_CHECKING:
14
20
  from google import genai
@@ -27,23 +33,11 @@ def _genai_client() -> 'genai.client.Client':
27
33
 
28
34
  @pxt.udf(resource_pool='request-rate:gemini')
29
35
  async def generate_content(
30
- contents: str,
31
- *,
32
- model: str,
33
- candidate_count: Optional[int] = None,
34
- stop_sequences: Optional[list[str]] = None,
35
- max_output_tokens: Optional[int] = None,
36
- temperature: Optional[float] = None,
37
- top_p: Optional[float] = None,
38
- top_k: Optional[int] = None,
39
- response_mime_type: Optional[str] = None,
40
- response_schema: Optional[dict] = None,
41
- presence_penalty: Optional[float] = None,
42
- frequency_penalty: Optional[float] = None,
36
+ contents: str, *, model: str, config: Optional[dict] = None, tools: Optional[list[dict]] = None
43
37
  ) -> dict:
44
38
  """
45
39
  Generate content from the specified model. For additional details, see:
46
- <https://ai.google.dev/gemini-api/docs>
40
+ <https://ai.google.dev/gemini-api/docs/text-generation>
47
41
 
48
42
  Request throttling:
49
43
  Applies the rate limit set in the config (section `gemini`, key `rate_limit`). If no rate
@@ -56,38 +50,176 @@ async def generate_content(
56
50
  Args:
57
51
  contents: The input content to generate from.
58
52
  model: The name of the model to use.
59
-
60
- For details on the other parameters, see: <https://ai.google.dev/gemini-api/docs>
53
+ config: Configuration for generation, corresponding to keyword arguments of
54
+ `genai.types.GenerateContentConfig`. For details on the parameters, see:
55
+ <https://googleapis.github.io/python-genai/genai.html#module-genai.types>
56
+ tools: An optional list of Pixeltable tools to use. It is also possible to specify tools manually via the
57
+ `config['tools']` parameter, but at most one of `config['tools']` or `tools` may be used.
61
58
 
62
59
  Returns:
63
60
  A dictionary containing the response and other metadata.
64
61
 
65
62
  Examples:
66
- Add a computed column that applies the model `gemini-1.5-flash`
63
+ Add a computed column that applies the model `gemini-2.0-flash`
67
64
  to an existing Pixeltable column `tbl.prompt` of the table `tbl`:
68
65
 
69
- >>> tbl.add_computed_column(response=generate_content(tbl.prompt, model='gemini-1.5-flash'))
66
+ >>> tbl.add_computed_column(response=generate_content(tbl.prompt, model='gemini-2.0-flash'))
70
67
  """
71
68
  env.Env.get().require_package('google.genai')
72
69
  from google.genai import types
73
70
 
74
- config = types.GenerateContentConfig(
75
- candidate_count=candidate_count,
76
- stop_sequences=stop_sequences,
77
- max_output_tokens=max_output_tokens,
78
- temperature=temperature,
79
- top_p=top_p,
80
- top_k=top_k,
81
- response_mime_type=response_mime_type,
82
- response_schema=response_schema,
83
- presence_penalty=presence_penalty,
84
- frequency_penalty=frequency_penalty,
85
- )
86
-
87
- response = await _genai_client().aio.models.generate_content(model=model, contents=contents, config=config)
71
+ config_: types.GenerateContentConfig
72
+ if config is None and tools is None:
73
+ config_ = None
74
+ else:
75
+ if config is None:
76
+ config_ = types.GenerateContentConfig()
77
+ else:
78
+ config_ = types.GenerateContentConfig(**config)
79
+ if tools is not None:
80
+ gemini_tools = [__convert_pxt_tool(tool) for tool in tools]
81
+ config_.tools = [types.Tool(function_declarations=gemini_tools)]
82
+
83
+ response = await _genai_client().aio.models.generate_content(model=model, contents=contents, config=config_)
88
84
  return response.model_dump()
89
85
 
90
86
 
87
+ def __convert_pxt_tool(pxt_tool: dict) -> dict:
88
+ return {
89
+ 'name': pxt_tool['name'],
90
+ 'description': pxt_tool['description'],
91
+ 'parameters': {
92
+ 'type': 'object',
93
+ 'properties': pxt_tool['parameters']['properties'],
94
+ 'required': pxt_tool['required'],
95
+ },
96
+ }
97
+
98
+
99
+ def invoke_tools(tools: pxt.func.Tools, response: exprs.Expr) -> exprs.InlineDict:
100
+ """Converts an OpenAI response dict to Pixeltable tool invocation format and calls `tools._invoke()`."""
101
+ return tools._invoke(_gemini_response_to_pxt_tool_calls(response))
102
+
103
+
104
+ @pxt.udf
105
+ def _gemini_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
106
+ pxt_tool_calls: dict[str, list[dict]] = {}
107
+ for part in response['candidates'][0]['content']['parts']:
108
+ tool_call = part.get('function_call')
109
+ if tool_call is not None:
110
+ tool_name = tool_call['name']
111
+ if tool_name not in pxt_tool_calls:
112
+ pxt_tool_calls[tool_name] = []
113
+ pxt_tool_calls[tool_name].append({'args': tool_call['args']})
114
+ if len(pxt_tool_calls) == 0:
115
+ return None
116
+ return pxt_tool_calls
117
+
118
+
91
119
  @generate_content.resource_pool
92
120
  def _(model: str) -> str:
93
121
  return f'request-rate:gemini:{model}'
122
+
123
+
124
+ @pxt.udf(resource_pool='request-rate:imagen')
125
+ async def generate_images(prompt: str, *, model: str, config: Optional[dict] = None) -> PIL.Image.Image:
126
+ """
127
+ Generates images based on a text description and configuration. For additional details, see:
128
+ <https://ai.google.dev/gemini-api/docs/image-generation>
129
+
130
+ __Requirements:__
131
+
132
+ - `pip install google-genai`
133
+
134
+ Args:
135
+ prompt: A text description of the images to generate.
136
+ model: The model to use.
137
+ config: Configuration for generation, corresponding to keyword arguments of
138
+ `genai.types.GenerateImagesConfig`. For details on the parameters, see:
139
+ <https://googleapis.github.io/python-genai/genai.html#module-genai.types>
140
+
141
+ Returns:
142
+ The generated image.
143
+
144
+ Examples:
145
+ Add a computed column that applies the model `imagen-3.0-generate-002`
146
+ to an existing Pixeltable column `tbl.prompt` of the table `tbl`:
147
+
148
+ >>> tbl.add_computed_column(response=generate_images(tbl.prompt, model='imagen-3.0-generate-002'))
149
+ """
150
+ env.Env.get().require_package('google.genai')
151
+ from google.genai.types import GenerateImagesConfig
152
+
153
+ config_ = GenerateImagesConfig(**config) if config else None
154
+ response = await _genai_client().aio.models.generate_images(model=model, prompt=prompt, config=config_)
155
+ return response.generated_images[0].image._pil_image
156
+
157
+
158
+ @generate_images.resource_pool
159
+ def _(model: str) -> str:
160
+ return f'request-rate:imagen:{model}'
161
+
162
+
163
+ @pxt.udf(resource_pool='request-rate:veo')
164
+ async def generate_videos(
165
+ prompt: Optional[str] = None, image: Optional[PIL.Image.Image] = None, *, model: str, config: Optional[dict] = None
166
+ ) -> pxt.Video:
167
+ """
168
+ Generates videos based on a text description and configuration. For additional details, see:
169
+ <https://ai.google.dev/gemini-api/docs/video-generation>
170
+
171
+ __Requirements:__
172
+
173
+ - `pip install google-genai`
174
+
175
+ Args:
176
+ prompt: A text description of the videos to generate.
177
+ image: An optional image to use as the first frame of the video. At least one of `prompt` or `image` must be
178
+ provided. (It is ok to specify both.)
179
+ model: The model to use.
180
+ config: Configuration for generation, corresponding to keyword arguments of
181
+ `genai.types.GenerateVideosConfig`. For details on the parameters, see:
182
+ <https://googleapis.github.io/python-genai/genai.html#module-genai.types>
183
+
184
+ Returns:
185
+ The generated video.
186
+
187
+ Examples:
188
+ Add a computed column that applies the model `veo-2.0-generate-001`
189
+ to an existing Pixeltable column `tbl.prompt` of the table `tbl`:
190
+
191
+ >>> tbl.add_computed_column(response=generate_videos(tbl.prompt, model='veo-2.0-generate-001'))
192
+ """
193
+ env.Env.get().require_package('google.genai')
194
+ from google.genai import types
195
+
196
+ if prompt is None and image is None:
197
+ raise excs.Error('At least one of `prompt` or `image` must be provided.')
198
+
199
+ image_: Optional[types.Image] = None
200
+ if image is not None:
201
+ with io.BytesIO() as buffer:
202
+ image.save(buffer, format='jpeg')
203
+ image_ = types.Image(image_bytes=buffer.getvalue(), mime_type='image/jpeg')
204
+
205
+ config_ = types.GenerateVideosConfig(**config) if config else None
206
+ operation = await _genai_client().aio.models.generate_videos(
207
+ model=model, prompt=prompt, image=image_, config=config_
208
+ )
209
+ while not operation.done:
210
+ await asyncio.sleep(3)
211
+ operation = await _genai_client().aio.operations.get(operation)
212
+
213
+ video = operation.response.generated_videos[0]
214
+
215
+ video_bytes = await _genai_client().aio.files.download(file=video.video) # type: ignore[arg-type]
216
+ assert video_bytes is not None
217
+
218
+ _, output_filename = tempfile.mkstemp(suffix='.mp4', dir=str(env.Env.get().tmp_dir))
219
+ Path(output_filename).write_bytes(video_bytes)
220
+ return output_filename
221
+
222
+
223
+ @generate_videos.resource_pool
224
+ def _(model: str) -> str:
225
+ return f'request-rate:veo:{model}'