DuckDB Extension¶
The DuckDB integration provides SQL UDFs for responses, embeddings, prepared tasks, parsing, and token counting. The table below shows how to use the main pandas features from DuckDB.
| pandas feature | DuckDB equivalent |
|---|---|
Series.ai.responses() |
responses_udf() |
Series.ai.embeddings() |
embeddings_udf() |
Series.ai.task() |
task_udf() |
Series.ai.parse() with an explicit model |
parse_udf(..., response_format=Model) |
Series.ai.parse() with schema inference |
parse_udf(..., example_table_name=..., example_field_name=...) |
Series.ai.infer_schema() / DataFrame.ai.infer_schema() |
infer_schema() on a text column or a view of JSON rows |
Series.ai.count_tokens() |
count_tokens_udf() |
Series.ai.extract() |
SQL STRUCT field access |
DataFrame.ai.responses() / .task() / .parse() |
Pass to_json(t) from a table alias to the UDF; use a view of those JSON rows for schema inference |
DataFrame.ai.similarity() |
SQL list_cosine_similarity(a, b) |
The AI-based DataFrame.ai.fillna() builds examples from the whole DataFrame
and predicts only missing rows. There is no dedicated DuckDB wrapper for that
table-level operation; COALESCE is useful for ordinary SQL defaults but does
not perform AI imputation.
The DuckDB UDF, schema, token, similarity, and type implementations live in
the openaivec.duckdb_ext package. Persistent caching is a core BatchCache
feature; its DuckDB backend remains in openaivec._cache:
from openaivec._cache import DuckDBCacheBackend
backend = DuckDBCacheBackend.of("results.duckdb")
# Pass backend as the cache argument when constructing BatchCache.
Cache table names are single unqualified SQL identifiers matching
[A-Za-z_][A-Za-z_0-9]*; reserved words are supported. Keys can be strings,
integers, booleans, floats, bytes, or tuples of those types. Keys are encoded
with type tags, so 1 and "1" persist independently; other key types raise
TypeError. The current typed-key table schema is incompatible with tables
created by older releases. Opening an old cache table raises ValueError
instead of silently misreading its entries; choose a new table name or
explicitly migrate/rebuild the old data. The backend also provides get_many,
put_many, and touch_many for bounded bulk SQL operations. get_many does
not update LRU order; call touch_many after consuming the fetched keys.
Because get_many returns a Python dictionary, requesting distinct keys that
compare equal in Python (for example, 1 and True) together raises
ValueError rather than silently combining them; individual lookups remain
distinct.
Schema inference and parsing¶
import duckdb
from openaivec.duckdb_ext import infer_schema, parse_udf
conn = duckdb.connect()
conn.execute("CREATE TABLE reviews (review VARCHAR)")
conn.executemany(
"INSERT INTO reviews VALUES (?)",
[("The camera is excellent. 5 stars.",), ("The battery failed. 1 star.",)],
)
# Infer once and reuse the resulting model and prompt for multiple UDFs.
schema = infer_schema(
conn,
instructions="Extract product feedback and rating.",
example_table_name="reviews",
example_field_name="review",
)
parse_udf(
conn,
"parse_review",
instructions=schema.inference_prompt,
response_format=schema.model,
)
conn.sql("SELECT parse_review(review) AS parsed FROM reviews")
# Or infer and register in one call.
parse_udf(
conn,
"parse_review_auto",
instructions="Extract product feedback and rating.",
example_table_name="reviews",
example_field_name="review",
)
conn.sql("SELECT parse_review_auto(review) AS parsed FROM reviews")
parse_udf infers the schema when it is registered. The sample excludes SQL
NULL values and is bounded by max_examples (default 100). An explicit
response_format skips inference. max_retries controls total inference
attempts; max_validation_retries controls additional extraction corrections.
Token counts and complete rows¶
from openaivec.duckdb_ext import count_tokens_udf, parse_udf, responses_udf
count_tokens_udf(conn, "count_tokens")
conn.sql("SELECT review, count_tokens(review) AS num_tokens FROM reviews")
responses_udf(conn, "describe_row", instructions="Summarize the complete row.")
conn.sql("SELECT describe_row(to_json(t)) FROM reviews AS t")
# For DataFrame-style automatic parsing, infer from JSON rows and pass the
# same expression to the registered UDF.
conn.execute("CREATE VIEW review_rows AS SELECT to_json(t) AS input FROM reviews AS t")
parse_udf(
conn,
"parse_review_row",
instructions="Extract product feedback and rating from each row.",
example_table_name="review_rows",
example_field_name="input",
)
conn.sql("SELECT parse_review_row(to_json(t)) FROM reviews AS t")
count_tokens_udf uses the same configured tiktoken.Encoding as the pandas
accessor and returns SQL NULL for SQL NULL input. Structured outputs from
responses_udf, task_udf, and parse_udf are native DuckDB STRUCT values;
read fields with my_udf(text).field_name or expand a stored STRUCT column
with parsed.*.
Similarity search and generated DDL¶
similarity_search(conn, "documents", "queries", top_k=3) returns
query_id, query_text, target_text, and score, ordered by query row and
descending score. query_id is a 1-based position assigned during the query
table scan (not a persistent key or a guaranteed order across scans); duplicate
query text still receives an independent top-k ranking. top_k must be a
positive integer and is passed as a SQL parameter.
pydantic_to_duckdb_ddl(Model, "schema.table") quotes each table/column
identifier, including nested STRUCT fields. Table names may be unqualified or
qualified as schema.table or catalog.schema.table; use DuckDB double quotes
around a part to include a literal dot (for example, "my.schema".items).
Spaces and reserved words are supported. Embedded double quotes in field names
are escaped; SQL expressions and statement separators in identifiers are
rejected. Both typing.Optional[T] and T | None map to the same DuckDB and
Spark type; unions of distinct non-null types are unsupported.
openaivec.duckdb_ext ¶
DuckDB integration for openaivec.
Provides helpers that bridge openaivec's batched AI capabilities with DuckDB:
- UDF registration – register
responses,embeddings,task,parseandcount_tokensfunctions directly as DuckDB scalar UDFs for SQL queries. - Schema inference – infer a Pydantic response model once from a bounded
table sample before registering a typed
parseUDF. - Vector similarity –
similarity_searchperforms top-k cosine similarity queries against an embedding table using DuckDB's built-inlist_cosine_similarity. - Schema → DDL –
pydantic_to_duckdb_ddlconverts a Pydantic model to aCREATE TABLEstatement for immediate SQL analysis of structured-output results.
Quick Start¶
import duckdb
from openaivec.duckdb_ext import responses_udf, embeddings_udf
conn = duckdb.connect()
responses_udf(conn, "translate", instructions="Translate to French", reasoning={"effort": "none"})
embeddings_udf(conn, "embed")
conn.sql("SELECT translate(review) FROM products")
conn.sql("SELECT text, embed(text) FROM documents")
Functions:¶
infer_schema ¶
infer_schema(
conn: DuckDBPyConnection,
instructions: str,
example_table_name: str,
example_field_name: str,
max_examples: int = 100,
*,
max_retries: int = 8,
retry_policy: RetryPolicy | None = None,
**api_kwargs: Any,
) -> SchemaInferenceOutput
Infer a response schema from a bounded sample of a DuckDB column.
The table is resolved through DuckDB's relation API, which accepts regular, schema-qualified, and quoted table names without interpolating the name in SQL. NULL values are skipped before the sample limit is applied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
DuckDBPyConnection
|
Connection containing the example table. |
required |
instructions
|
str
|
Description of the information to extract. |
required |
example_table_name
|
str
|
Table or view containing examples. |
required |
example_field_name
|
str
|
Column containing example text. |
required |
max_examples
|
int
|
Maximum number of non-NULL examples. Must be positive. |
100
|
max_retries
|
int
|
Maximum schema inference attempts. Must be positive. |
8
|
retry_policy
|
RetryPolicy | None
|
Transport retry policy. |
None
|
**api_kwargs
|
Any
|
Extra parameters for schema inference API calls. |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
SchemaInferenceOutput |
SchemaInferenceOutput
|
Inferred prompt and Pydantic model. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If limits are invalid or the sample has no non-NULL values. |
Example
import duckdb conn = duckdb.connect() conn.sql("CREATE TABLE reviews(text VARCHAR)")
infer_schema(conn, "Extract sentiment", "reviews", "text")¶
Source code in src/openaivec/duckdb_ext/_schema.py
parse_udf ¶
parse_udf(
conn: DuckDBPyConnection,
name: str,
*,
instructions: str,
response_format: type[ResponseFormat] | None = None,
example_table_name: str | None = None,
example_field_name: str | None = None,
max_examples: int = 100,
model_name: str | None = None,
batch_size: int | None = 64,
max_concurrency: int = 8,
multimodal: bool = False,
max_retries: int = 8,
max_validation_retries: int = 3,
retry_policy: RetryPolicy | None = None,
**api_kwargs: Any,
) -> None
Register a DuckDB parse UDF with an explicit or inferred output schema.
DuckDB fixes UDF return types at registration. If response_format is
omitted, the schema is inferred once from example_table_name and
example_field_name before registering the UDF. The generated UDF then
returns a typed DuckDB STRUCT for each non-NULL input.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
DuckDBPyConnection
|
Connection on which to register the UDF. |
required |
name
|
str
|
UDF name visible in SQL. |
required |
instructions
|
str
|
Description of the information to parse. |
required |
response_format
|
type[ResponseFormat] | None
|
|
None
|
example_table_name
|
str | None
|
Source table for schema inference. |
None
|
example_field_name
|
str | None
|
Text column for schema inference. |
None
|
max_examples
|
int
|
Maximum non-NULL examples to inspect. |
100
|
model_name
|
str | None
|
Model or deployment name. |
None
|
batch_size
|
int | None
|
Inputs per API batch; None enables auto-tuning. |
64
|
max_concurrency
|
int
|
Maximum concurrent API requests. |
8
|
multimodal
|
bool
|
Whether file paths and URLs are multimodal inputs. |
False
|
max_retries
|
int
|
Maximum schema inference attempts. |
8
|
max_validation_retries
|
int
|
Additional extraction corrections. |
3
|
retry_policy
|
RetryPolicy | None
|
Transport retry policy. |
None
|
**api_kwargs
|
Any
|
Extra parameters for inference and parsing API calls. |
{}
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If limits are invalid or inference inputs are missing. |
TypeError
|
If an explicit response format is unsupported. |
Example
import duckdb from pydantic import BaseModel class Parsed(BaseModel): ... label: str conn = duckdb.connect() parse_udf(conn, "parse_label", instructions="Extract label", response_format=Parsed)
Source code in src/openaivec/duckdb_ext/_schema.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |
similarity_search ¶
similarity_search(
conn: DuckDBPyConnection,
target_table: str,
query_table: str,
*,
target_column: str = "embedding",
query_column: str = "embedding",
target_text_column: str = "text",
query_text_column: str = "text",
top_k: int = 10,
) -> duckdb.DuckDBPyRelation
Perform top-k cosine similarity search between two DuckDB tables.
Uses DuckDB's built-in list_cosine_similarity for efficient
vector comparison without leaving SQL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
DuckDBPyConnection
|
An open DuckDB connection. |
required |
target_table
|
str
|
Table containing candidate embeddings. |
required |
query_table
|
str
|
Table containing query embeddings. |
required |
target_column
|
str
|
Embedding column in target_table. |
'embedding'
|
query_column
|
str
|
Embedding column in query_table. |
'embedding'
|
target_text_column
|
str
|
Text identifier column in target_table. |
'text'
|
query_text_column
|
str
|
Text identifier column in query_table. |
'text'
|
top_k
|
int
|
Number of results per query. |
10
|
Returns:
| Type | Description |
|---|---|
DuckDBPyRelation
|
duckdb.DuckDBPyRelation: A DuckDB relation with |
DuckDBPyRelation
|
query row position), |
DuckDBPyRelation
|
ordered by query row and descending similarity. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an identifier or |
Example
import duckdb from openaivec.duckdb_ext import similarity_search conn = duckdb.connect()
(after populating docs and queries tables with embeddings)¶
results = similarity_search(conn, "docs", "queries", top_k=5) results.df()
Source code in src/openaivec/duckdb_ext/_similarity.py
count_tokens_udf ¶
Register a vectorized DuckDB UDF that counts tokens in text values.
The tokenizer is the same configured tiktoken.Encoding used by
pandas.Series.ai.count_tokens. SQL NULL values remain NULL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
DuckDBPyConnection
|
Connection on which to register the UDF. |
required |
name
|
str
|
Function name visible in SQL. |
required |
Example
import duckdb from openaivec.duckdb_ext import count_tokens_udf conn = duckdb.connect() count_tokens_udf(conn, "token_count") conn.sql("SELECT token_count('hello')").fetchone()[0] > 0 True
Source code in src/openaivec/duckdb_ext/_tokens.py
pydantic_to_duckdb_ddl ¶
Generate a CREATE TABLE DDL statement from a Pydantic model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
type[BaseModel]
|
The Pydantic model class. |
required |
table_name
|
str
|
Name for the DuckDB table. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
A |
Example
from pydantic import BaseModel from openaivec.duckdb_ext import pydantic_to_duckdb_ddl class Review(BaseModel): ... sentiment: str ... rating: int ... tags: list[str] print(pydantic_to_duckdb_ddl(Review, "reviews")) CREATE TABLE IF NOT EXISTS "reviews" ( "sentiment" VARCHAR, "rating" INTEGER, "tags" VARCHAR[] )
Source code in src/openaivec/duckdb_ext/_types.py
embeddings_udf ¶
embeddings_udf(
conn: DuckDBPyConnection,
name: str,
*,
model_name: str | None = None,
batch_size: int = 128,
max_concurrency: int = 8,
limits: EmbeddingLimits | None = None,
retry_policy: RetryPolicy | None = None,
**api_kwargs: Any,
) -> None
Register a DuckDB Arrow-based UDF that returns embedding vectors.
The UDF processes rows in vectorized batches via AsyncBatchEmbeddings,
leveraging deduplication and concurrency for throughput.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
DuckDBPyConnection
|
An open DuckDB connection. |
required |
name
|
str
|
UDF name visible in SQL. |
required |
model_name
|
str | None
|
Embeddings model or deployment name. |
None
|
batch_size
|
int
|
Rows per API batch. Defaults to 128. |
128
|
max_concurrency
|
int
|
Maximum concurrent API requests. Defaults to 8. |
8
|
limits
|
EmbeddingLimits | None
|
Hard provider limits; None uses OpenAI defaults. |
None
|
retry_policy
|
RetryPolicy | None
|
Transport limits. |
None
|
**api_kwargs
|
Any
|
Extra parameters forwarded to the OpenAI API. |
{}
|
Example
import duckdb from openaivec.duckdb_ext import embeddings_udf conn = duckdb.connect() embeddings_udf(conn, "embed")
conn.sql("SELECT embed(text) FROM docs")¶
Source code in src/openaivec/duckdb_ext/_udfs.py
responses_udf ¶
responses_udf(
conn: DuckDBPyConnection,
name: str,
*,
instructions: str,
response_format: type = str,
model_name: str | None = None,
batch_size: int | None = 64,
max_concurrency: int = 8,
multimodal: bool = False,
max_validation_retries: int = 3,
retry_policy: RetryPolicy | None = None,
**api_kwargs: Any,
) -> None
Register a DuckDB Arrow-based UDF that calls the OpenAI Responses API.
The UDF processes rows in vectorized batches via AsyncBatchResponses,
leveraging deduplication and concurrency for throughput.
When response_format is a Pydantic BaseModel, the UDF returns a
DuckDB STRUCT whose fields match the model, allowing direct field
access in SQL (e.g. SELECT udf(text).sentiment FROM ...).
When response_format is str, the UDF returns VARCHAR.
SQL NULL inputs are not sent to the API. Missing parsed responses remain SQL NULL, including structured outputs; all-NULL Arrow batches retain the declared return type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
DuckDBPyConnection
|
An open DuckDB connection. |
required |
name
|
str
|
UDF name visible in SQL. |
required |
instructions
|
str
|
System prompt for the model. |
required |
response_format
|
type
|
|
str
|
model_name
|
str | None
|
Model or deployment name. Defaults to the
container-registered |
None
|
batch_size
|
int | None
|
Rows per API batch; None enables auto-tuning. Defaults to 64. |
64
|
max_concurrency
|
int
|
Maximum concurrent API requests. Defaults to 8. |
8
|
max_validation_retries
|
int
|
Additional schema/ID corrections per batch. Defaults to 3; 0 disables correction. Must be nonnegative. |
3
|
retry_policy
|
RetryPolicy | None
|
Transport limits. |
None
|
**api_kwargs
|
Any
|
Extra parameters forwarded to the OpenAI API. |
{}
|
Example
import duckdb from pydantic import BaseModel from openaivec.duckdb_ext import responses_udf class Sentiment(BaseModel): ... label: str ... score: float conn = duckdb.connect() responses_udf(conn, "sentiment", instructions="Analyze sentiment", response_format=Sentiment)
conn.sql("SELECT sentiment(text).label, sentiment(text).score FROM docs")¶
Source code in src/openaivec/duckdb_ext/_udfs.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | |
task_udf ¶
task_udf(
conn: DuckDBPyConnection,
name: str,
*,
task: PreparedTask[ResponseFormat],
model_name: str | None = None,
batch_size: int | None = 64,
max_concurrency: int = 8,
multimodal: bool = False,
max_validation_retries: int = 3,
retry_policy: RetryPolicy | None = None,
**api_kwargs: Any,
) -> None
Register a DuckDB UDF backed by a PreparedTask.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
DuckDBPyConnection
|
An open DuckDB connection. |
required |
name
|
str
|
UDF name visible in SQL. |
required |
task
|
PreparedTask
|
Pre-configured task with instructions and response format. |
required |
model_name
|
str | None
|
Model or deployment name. |
None
|
batch_size
|
int | None
|
Rows per API batch; None enables auto-tuning. Defaults to 64. |
64
|
max_concurrency
|
int
|
Maximum concurrent API requests. Defaults to 8. |
8
|
max_validation_retries
|
int
|
Additional schema/ID corrections per batch. Defaults to 3; 0 disables correction. Must be nonnegative. |
3
|
multimodal
|
bool
|
When |
False
|
retry_policy
|
RetryPolicy | None
|
Transport limits. |
None
|
**api_kwargs
|
Any
|
Extra parameters forwarded to the OpenAI API. |
{}
|