An alpha connector for storing and searching native VECTOR columns in Oracle
AI Database 26ai. OracleCollection implements async batch CRUD and vector
search; OracleStore shares a client across collections; OracleSettings
describes the connection and pool settings resolved through Agent Framework.
pip install agent-framework-oracle --preRequires Python 3.10+, Oracle AI Database 26ai with COMPATIBLE set to
23.4.0 or higher, python-oracledb 2.2.x or 3.x, and permission to
create/drop tables in the connected user's schema. The connector uses the
driver's async Thin-mode API; it does not initialize the Thick client or
provision a database.
It does not create or alter schemas, vector indexes, or existing tables.
For username/password authentication, set ORACLE_DSN (for example
localhost:1521/FREEPDB1), ORACLE_USER, and ORACLE_PASSWORD, or pass
dsn, user, and password to either constructor. Typed driver settings can
also be supplied with the pool_parameters mapping. Settings are resolved in
order: named dsn/user/password argument > pool_parameters > selected
.env file > process environment > connector pool defaults. A .env file is
read only when env_file_path is specified; the selected file must exist, and
env_file_encoding overrides UTF-8. password accepts str or Agent Framework
SecretString and is unwrapped only when opening a connection.
Named connection values must be nonempty when supplied, but their presence is
not required by the connector. Authentication and connection-descriptor
completeness are validated by python-oracledb, allowing alternatives such as
access_token, externalauth, or host with service_name/sid.
Explicit pool_parameters keys are forwarded when they are accepted by the
installed python-oracledb version, including newly added driver options not
yet declared by the open OracleSettings typed dictionary. This preserves
editor discovery for known options while allowing forward-compatible keys.
Unsupported names and typos raise an error; unrecognized ORACLE_ environment
variables remain ignored.
Recognized scalar oracledb.create_pool_async() options can also be supplied as
ORACLE_ settings. The prefix is removed, the remainder is converted to the
driver's lowercase keyword name, and boolean, integer, and floating-point
values are converted before the pool is created. Password-like values remain
masked until they are passed to the driver. For example:
ORACLE_DSN=localhost:1521/FREEPDB1
ORACLE_USER=example
ORACLE_PASSWORD=database-password
ORACLE_WALLET_LOCATION=/path/to/wallet
ORACLE_WALLET_PASSWORD=wallet-password
ORACLE_MIN=1
ORACLE_MAX=8
ORACLE_HOMOGENEOUS=falseThe connector uses min=0 and max=4 when those options are not configured.
Unrecognized ORACLE_ variables are ignored. Options that require callbacks,
classes, collections, or SSL context objects are not loaded from text settings;
pass them through pool_parameters or use a configured client.
Alternatively, pass a configured oracledb.AsyncConnection or
oracledb.AsyncConnectionPool as client for advanced authentication. A
supplied client cannot be combined with connection settings and remains
caller-owned. A connector-created pool opens on first use and closes on
close() or async context exit. A collection obtained from a store borrows its
client; closing that collection does not close the store. The store must remain
open while its collections are used.
Writes on pooled connections (both connector-owned and caller-supplied pools)
are committed on success and rolled back on failure. Writes to a supplied
AsyncConnection are left in the caller's transaction; the caller must
commit or roll back. Oracle DDL commits independently of the surrounding
transaction. Batch upserts on a borrowed connection may leave partial pending
work on failure, so roll it back before reuse.
ensure_collection_exists() leaves an existing table unchanged and does not
validate its schema; use a matching definition or create a new table. It never
migrates data or changes existing indexes.
import asyncio
from dataclasses import dataclass
from typing import Annotated
from agent_framework import Filter, VectorStoreField, vectorstoremodel
from agent_framework_oracle import OracleStore
@vectorstoremodel(collection_name="articles")
@dataclass
class Article:
id: Annotated[str, VectorStoreField("key")]
text: Annotated[str, VectorStoreField("data")]
embedding: Annotated[list[float] | None, VectorStoreField("vector", dimensions=3)] = None
async def main() -> None:
async with OracleStore() as store:
collection = store.get_collection(Article)
await collection.ensure_collection_exists()
await collection.upsert([Article("one", "Oracle vector search", [1, 0, 0])], generate_vectors=False)
results = await collection.search(
vector=[1, 0, 0], filter=Filter("text", "contains_text", "Oracle"), top=3
)
async for result in results:
print(result["record"].text, result["score"])
asyncio.run(main())Pass generate_vectors=False for precomputed vectors, or configure an
embedding_generator for local generation. get() excludes vector columns by
default; pass include_vectors=True to restore them. get(keys) preserves
requested key order and duplicates, omitting missing keys; filtered retrieval
supports top, skip, and order_by. Both get() and search() execute
portable filters and paging in Oracle. The
runnable sample
creates and deletes a unique test table.
- A collection is one table in the current user's schema. Keys are application-provided strings (up to 512 UTF-8 bytes), signed 64-bit integers, or UUIDs; automatically generated keys are not supported.
- Multiple nullable native vector columns are supported. Vector fields may
declare
float/float32(OracleFLOAT32),float64(FLOAT64), orint8(INT8); omitted element types useFLOAT32. Dimensions must be 1–65535. Supplied vectors must be dense finite numeric sequences with the declared dimensions. Binary and sparse vectors are not supported. - Scalar data fields support
str(up to 4000 UTF-8 bytes),int(signed 64-bit),float(BINARY_DOUBLE), andbool(stored asNUMBER(1,0)). Oracle converts empty strings toNULL, so this connector rejects empty string values rather than changing their meaning. JSON fields, nested paths, provider annotations, data/full-text/vector indexes, and schema migration are not supported. - Search uses Oracle
VECTOR_DISTANCEwith an explicit metric. The default isCOSINEdistance;cosine_similarity, Euclidean distance, dot product, negative dot product, squared Euclidean distance, and Manhattan distance are also available. Scores are native metric values, not probabilities. For distances ascore_thresholdis a maximum; for similarities/dot product it is a minimum. The cutoff is applied in SQL before paging. No ANN indexes are created or managed; Oracle may use an existing index if the table already has one. There is no exact/approximate toggle, server-side embedding generation, or keyword-hybrid search. Index-selected approximate searches may return fewer than the requested number of results. - Portable filters support scalar equality (including
NULLand boolean distinctions),in/not_in, numeric ranges,is_null,is_not_null,exists, literal textcontains_text/starts_with/ends_with, and AND/OR/NOT. FiniteDecimalvalues are supported for exact numeric equality and membership; malformed UUID filter values compare as unequal. Collection membership and nested filters are rejected. Ordered filters on integer fields reject non-integral float operands; orderedDecimaloperands are not supported. String comparison follows the configured Oracle collation. Input values are bound; identifiers are checked and quoted as individual names.
The unit tests require no Oracle server. To run the live integration test,
provide all three ORACLE_TEST_DSN, ORACLE_TEST_USER, and
ORACLE_TEST_PASSWORD for an explicitly designated disposable Oracle AI
Database 26ai schema where the user can create and drop tables:
cd python
uv run --package agent-framework-oracle pytest packages/oracle/tests \
-m integrationWithout those variables, the integration test skips; it never falls back to ordinary application credentials. The fixture creates a unique table and deletes only that table.