agent-framework-oracle 1.0.0a261008__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.
- agent_framework_oracle/__init__.py +16 -0
- agent_framework_oracle/_vector_store.py +944 -0
- agent_framework_oracle/py.typed +0 -0
- agent_framework_oracle-1.0.0a261008.dist-info/METADATA +175 -0
- agent_framework_oracle-1.0.0a261008.dist-info/RECORD +7 -0
- agent_framework_oracle-1.0.0a261008.dist-info/WHEEL +4 -0
- agent_framework_oracle-1.0.0a261008.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Oracle Database 23ai native-vector collections and stores."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
|
|
9
|
+
from ._vector_store import OracleCollection, OracleSettings, OracleStore
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = importlib.metadata.version(__name__)
|
|
13
|
+
except importlib.metadata.PackageNotFoundError:
|
|
14
|
+
__version__ = "0.0.0"
|
|
15
|
+
|
|
16
|
+
__all__ = ["OracleCollection", "OracleSettings", "OracleStore", "__version__"]
|
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Oracle Database 23ai vector collections and stores."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import math
|
|
9
|
+
from array import array
|
|
10
|
+
from collections.abc import AsyncGenerator, Collection, Mapping, Sequence
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from typing import Any, ClassVar, Generic, Protocol, TypeAlias, cast
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
import oracledb
|
|
17
|
+
from agent_framework import (
|
|
18
|
+
BaseVectorCollection,
|
|
19
|
+
BaseVectorSearch,
|
|
20
|
+
BaseVectorStore,
|
|
21
|
+
FilterGroup,
|
|
22
|
+
SearchResults,
|
|
23
|
+
SecretString,
|
|
24
|
+
VectorStoreCollectionDefinition,
|
|
25
|
+
VectorStoreField,
|
|
26
|
+
load_settings,
|
|
27
|
+
)
|
|
28
|
+
from agent_framework._vector_filters import FilterExpression
|
|
29
|
+
from agent_framework._vectors import EmbeddingClient, SearchType, Vector
|
|
30
|
+
from agent_framework.exceptions import IntegrationException, IntegrationInvalidResponseException
|
|
31
|
+
from typing_extensions import Self, TypedDict, TypeVar
|
|
32
|
+
|
|
33
|
+
KeyT = TypeVar("KeyT", default=Any)
|
|
34
|
+
ModelT = TypeVar("ModelT", default=Any)
|
|
35
|
+
OracleClient: TypeAlias = oracledb.AsyncConnection | oracledb.AsyncConnectionPool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _OracleCursor(Protocol):
|
|
39
|
+
"""Typed boundary for python-oracledb's partially annotated async cursor."""
|
|
40
|
+
|
|
41
|
+
async def __aenter__(self) -> Self: ...
|
|
42
|
+
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: ...
|
|
43
|
+
async def execute(self, statement: str, parameters: Mapping[str, Any] | None = None) -> None: ...
|
|
44
|
+
async def executemany(self, statement: str, parameters: Sequence[Mapping[str, Any]]) -> None: ...
|
|
45
|
+
async def fetchone(self) -> Sequence[Any] | None: ...
|
|
46
|
+
async def fetchall(self) -> list[Sequence[Any]]: ...
|
|
47
|
+
def setinputsizes(self, **kwargs: Any) -> Any: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_MAX_IDENTIFIER_BYTES = 128
|
|
51
|
+
_MAX_KEY_BYTES = 512
|
|
52
|
+
_MAX_TEXT_BYTES = 4000
|
|
53
|
+
_BATCH_SIZE = 500
|
|
54
|
+
_METRICS = {
|
|
55
|
+
"DEFAULT": "COSINE",
|
|
56
|
+
"cosine_distance": "COSINE",
|
|
57
|
+
"cosine_similarity": "COSINE",
|
|
58
|
+
"euclidean_distance": "EUCLIDEAN",
|
|
59
|
+
"euclidean_squared_distance": "EUCLIDEAN_SQUARED",
|
|
60
|
+
"manhattan": "MANHATTAN",
|
|
61
|
+
"negative_dot_prod": "DOT",
|
|
62
|
+
"dot_prod": "DOT",
|
|
63
|
+
}
|
|
64
|
+
_ORDER_OPERATORS = {"gt": ">", "gte": ">=", "lt": "<", "lte": "<="}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class OracleSettings(TypedDict, total=False):
|
|
68
|
+
"""Connection settings resolved from arguments, a selected .env file, or ``ORACLE_`` variables."""
|
|
69
|
+
|
|
70
|
+
dsn: str | None
|
|
71
|
+
user: str | None
|
|
72
|
+
password: SecretString | None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _quote_identifier(name: str) -> str:
|
|
76
|
+
if not isinstance(name, str) or not name or "\0" in name:
|
|
77
|
+
raise ValueError("Oracle identifiers must be nonempty strings without NUL.")
|
|
78
|
+
try:
|
|
79
|
+
length = len(name.encode("utf-8"))
|
|
80
|
+
except UnicodeEncodeError as exc:
|
|
81
|
+
raise ValueError("Oracle identifiers must be valid UTF-8.") from exc
|
|
82
|
+
if length > _MAX_IDENTIFIER_BYTES:
|
|
83
|
+
raise ValueError("Oracle identifiers cannot exceed 128 UTF-8 bytes.")
|
|
84
|
+
return '"' + name.replace('"', '""') + '"'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validate_operation_options(options: Mapping[str, Any] | None) -> None:
|
|
88
|
+
if options:
|
|
89
|
+
raise NotImplementedError("Oracle does not support operation_options.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _validate_scalar(field: VectorStoreField, value: Any) -> Any:
|
|
93
|
+
if value is None:
|
|
94
|
+
if field.field_type == "key":
|
|
95
|
+
raise ValueError("Oracle keys cannot be null.")
|
|
96
|
+
return None
|
|
97
|
+
kind = field.type_
|
|
98
|
+
if kind == "UUID":
|
|
99
|
+
if not isinstance(value, UUID | str):
|
|
100
|
+
raise TypeError(f"Field '{field.name}' requires a UUID.")
|
|
101
|
+
return str(UUID(str(value)))
|
|
102
|
+
if kind == "str":
|
|
103
|
+
if not isinstance(value, str):
|
|
104
|
+
raise TypeError(f"Field '{field.name}' requires a string.")
|
|
105
|
+
if not value:
|
|
106
|
+
raise ValueError("Oracle cannot store an empty string distinctly from NULL.")
|
|
107
|
+
limit = _MAX_KEY_BYTES if field.field_type == "key" else _MAX_TEXT_BYTES
|
|
108
|
+
if len(value.encode("utf-8")) > limit:
|
|
109
|
+
raise ValueError(f"Field '{field.name}' exceeds {limit} UTF-8 bytes.")
|
|
110
|
+
return value
|
|
111
|
+
if kind == "int":
|
|
112
|
+
if type(value) is not int:
|
|
113
|
+
raise TypeError(f"Field '{field.name}' requires an integer.")
|
|
114
|
+
if not -(2**63) <= value < 2**63:
|
|
115
|
+
raise ValueError(f"Field '{field.name}' exceeds the signed 64-bit integer range.")
|
|
116
|
+
return value
|
|
117
|
+
if kind == "float":
|
|
118
|
+
if type(value) not in (int, float):
|
|
119
|
+
raise TypeError(f"Field '{field.name}' requires a number.")
|
|
120
|
+
try:
|
|
121
|
+
number = float(value)
|
|
122
|
+
except OverflowError as exc:
|
|
123
|
+
raise ValueError(f"Field '{field.name}' requires a finite number.") from exc
|
|
124
|
+
if not math.isfinite(number):
|
|
125
|
+
raise ValueError(f"Field '{field.name}' requires a finite number.")
|
|
126
|
+
return number
|
|
127
|
+
if kind == "bool":
|
|
128
|
+
if type(value) is not bool:
|
|
129
|
+
raise TypeError(f"Field '{field.name}' requires a boolean.")
|
|
130
|
+
return int(value)
|
|
131
|
+
raise NotImplementedError(f"Oracle field type '{kind}' is not supported.")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _prepare_vector(field: VectorStoreField, value: Any) -> array[Any] | None:
|
|
135
|
+
if value is None:
|
|
136
|
+
return None
|
|
137
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
|
|
138
|
+
tolist = getattr(value, "tolist", None)
|
|
139
|
+
value = tolist() if callable(tolist) else value
|
|
140
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
|
|
141
|
+
raise TypeError(f"Oracle vector '{field.name}' requires a dense numeric sequence.")
|
|
142
|
+
elements = cast(Sequence[Any], value)
|
|
143
|
+
if len(elements) != field.dimensions:
|
|
144
|
+
raise ValueError(f"Oracle vector '{field.name}' expects {field.dimensions} dimensions; got {len(elements)}.")
|
|
145
|
+
element_type = field.type_ or "float32"
|
|
146
|
+
if element_type == "int8":
|
|
147
|
+
if any(type(item) is not int or not -128 <= item <= 127 for item in elements):
|
|
148
|
+
raise ValueError(f"Oracle vector '{field.name}' requires signed int8 elements.")
|
|
149
|
+
return array("b", elements)
|
|
150
|
+
result: list[float] = []
|
|
151
|
+
for item in elements:
|
|
152
|
+
if type(item) not in (int, float):
|
|
153
|
+
raise TypeError(f"Oracle vector '{field.name}' requires numeric elements, not booleans.")
|
|
154
|
+
try:
|
|
155
|
+
number = float(item)
|
|
156
|
+
except OverflowError as exc:
|
|
157
|
+
raise ValueError(f"Oracle vector '{field.name}' requires finite elements.") from exc
|
|
158
|
+
if not math.isfinite(number):
|
|
159
|
+
raise ValueError(f"Oracle vector '{field.name}' requires finite elements.")
|
|
160
|
+
result.append(number)
|
|
161
|
+
try:
|
|
162
|
+
prepared = array("d" if element_type == "float64" else "f", result)
|
|
163
|
+
except OverflowError as exc:
|
|
164
|
+
raise ValueError(f"Oracle vector '{field.name}' contains out-of-range elements.") from exc
|
|
165
|
+
if not all(math.isfinite(item) for item in prepared):
|
|
166
|
+
raise ValueError(f"Oracle vector '{field.name}' contains out-of-range elements.")
|
|
167
|
+
return prepared
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _column_type(field: VectorStoreField) -> str:
|
|
171
|
+
if field.field_type == "vector":
|
|
172
|
+
dimensions = field.dimensions
|
|
173
|
+
if type(dimensions) is not int or not 1 <= dimensions <= 65535:
|
|
174
|
+
raise ValueError("Oracle vector dimensions must be an integer between 1 and 65535.")
|
|
175
|
+
storage_type = {
|
|
176
|
+
None: "FLOAT32",
|
|
177
|
+
"float": "FLOAT32",
|
|
178
|
+
"float32": "FLOAT32",
|
|
179
|
+
"float64": "FLOAT64",
|
|
180
|
+
"int8": "INT8",
|
|
181
|
+
}[field.type_]
|
|
182
|
+
return f"VECTOR({dimensions}, {storage_type})"
|
|
183
|
+
if field.type_ is None:
|
|
184
|
+
raise ValueError(f"Oracle field '{field.name}' requires an explicit type.")
|
|
185
|
+
if field.field_type == "key":
|
|
186
|
+
return {"str": "VARCHAR2(512 BYTE)", "UUID": "VARCHAR2(36 BYTE)", "int": "NUMBER(19,0)"}[field.type_]
|
|
187
|
+
return {"str": "VARCHAR2(4000 BYTE)", "int": "NUMBER(19,0)", "float": "BINARY_DOUBLE", "bool": "NUMBER(1,0)"}[
|
|
188
|
+
field.type_
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _bind_type(field: VectorStoreField) -> Any:
|
|
193
|
+
if field.field_type == "vector":
|
|
194
|
+
return cast(Any, oracledb.DB_TYPE_VECTOR) # pyright: ignore[reportUnknownMemberType]
|
|
195
|
+
if field.type_ in ("str", "UUID"):
|
|
196
|
+
return (
|
|
197
|
+
_MAX_KEY_BYTES
|
|
198
|
+
if field.field_type == "key" and field.type_ == "str"
|
|
199
|
+
else (36 if field.type_ == "UUID" else _MAX_TEXT_BYTES)
|
|
200
|
+
)
|
|
201
|
+
if field.type_ == "float":
|
|
202
|
+
return cast(Any, oracledb.DB_TYPE_BINARY_DOUBLE) # pyright: ignore[reportUnknownMemberType]
|
|
203
|
+
return cast(Any, oracledb.DB_TYPE_NUMBER) # pyright: ignore[reportUnknownMemberType]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _oracle_error_code(exc: oracledb.DatabaseError) -> int | None:
|
|
207
|
+
error = exc.args[0] if exc.args else None
|
|
208
|
+
code = getattr(error, "code", None)
|
|
209
|
+
return code if isinstance(code, int) else None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class _Client:
|
|
213
|
+
"""Own a lazy async pool or borrow an async pool/connection."""
|
|
214
|
+
|
|
215
|
+
def __init__(
|
|
216
|
+
self,
|
|
217
|
+
*,
|
|
218
|
+
dsn: str | None = None,
|
|
219
|
+
user: str | None = None,
|
|
220
|
+
password: SecretString | None = None,
|
|
221
|
+
client: OracleClient | None = None,
|
|
222
|
+
) -> None:
|
|
223
|
+
if client is not None:
|
|
224
|
+
if not isinstance(client, (oracledb.AsyncConnection, oracledb.AsyncConnectionPool)):
|
|
225
|
+
raise TypeError("client must be an oracledb AsyncConnection or AsyncConnectionPool.")
|
|
226
|
+
if any(value is not None for value in (dsn, user, password)):
|
|
227
|
+
raise ValueError("An Oracle client cannot be combined with connection settings.")
|
|
228
|
+
else:
|
|
229
|
+
if not isinstance(dsn, str) or not dsn.strip():
|
|
230
|
+
raise ValueError("A nonempty Oracle DSN is required.")
|
|
231
|
+
if not isinstance(user, str) or not user.strip():
|
|
232
|
+
raise ValueError("A nonempty Oracle user is required.")
|
|
233
|
+
if not isinstance(password, SecretString) or not password.get_secret_value():
|
|
234
|
+
raise ValueError("A nonempty Oracle password is required.")
|
|
235
|
+
self._client = client
|
|
236
|
+
self._dsn = dsn
|
|
237
|
+
self._user = user
|
|
238
|
+
self._password = password
|
|
239
|
+
self._pool: oracledb.AsyncConnectionPool | None = None
|
|
240
|
+
self._lock = asyncio.Lock()
|
|
241
|
+
self._closed = False
|
|
242
|
+
|
|
243
|
+
def ensure_open(self) -> None:
|
|
244
|
+
if self._closed:
|
|
245
|
+
raise RuntimeError("The Oracle client is closed.")
|
|
246
|
+
|
|
247
|
+
async def _get_client(self) -> OracleClient:
|
|
248
|
+
self.ensure_open()
|
|
249
|
+
if self._client is not None:
|
|
250
|
+
return self._client
|
|
251
|
+
async with self._lock:
|
|
252
|
+
self.ensure_open()
|
|
253
|
+
if self._pool is None:
|
|
254
|
+
if self._user is None or self._dsn is None or self._password is None:
|
|
255
|
+
raise RuntimeError("Oracle connection settings are missing.")
|
|
256
|
+
self._pool = oracledb.create_pool_async(
|
|
257
|
+
user=self._user, password=self._password.get_secret_value(), dsn=self._dsn, min=0, max=4
|
|
258
|
+
)
|
|
259
|
+
return self._pool
|
|
260
|
+
|
|
261
|
+
@asynccontextmanager
|
|
262
|
+
async def connection(self, *, write: bool = False) -> AsyncGenerator[oracledb.AsyncConnection]:
|
|
263
|
+
"""Commit or roll back pooled writes; a borrowed connection remains caller-transactional."""
|
|
264
|
+
try:
|
|
265
|
+
client = await self._get_client()
|
|
266
|
+
if isinstance(client, oracledb.AsyncConnectionPool):
|
|
267
|
+
async with client.acquire() as connection: # pyright: ignore[reportUnknownMemberType]
|
|
268
|
+
try:
|
|
269
|
+
yield connection
|
|
270
|
+
if write:
|
|
271
|
+
await connection.commit()
|
|
272
|
+
except (Exception, asyncio.CancelledError):
|
|
273
|
+
if write:
|
|
274
|
+
await connection.rollback()
|
|
275
|
+
raise
|
|
276
|
+
else:
|
|
277
|
+
yield client
|
|
278
|
+
except oracledb.Error as exc:
|
|
279
|
+
raise IntegrationException("Oracle operation failed; inspect the chained driver exception.") from exc
|
|
280
|
+
|
|
281
|
+
async def close(self) -> None:
|
|
282
|
+
"""Close only a connector-created pool; a borrowed client remains open."""
|
|
283
|
+
async with self._lock:
|
|
284
|
+
if not self._closed:
|
|
285
|
+
if self._pool is not None:
|
|
286
|
+
await self._pool.close()
|
|
287
|
+
self._closed = True
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _create_client(
|
|
291
|
+
*,
|
|
292
|
+
dsn: str | None,
|
|
293
|
+
user: str | None,
|
|
294
|
+
password: str | SecretString | None,
|
|
295
|
+
client: OracleClient | None,
|
|
296
|
+
env_file_path: str | None,
|
|
297
|
+
env_file_encoding: str | None,
|
|
298
|
+
) -> _Client:
|
|
299
|
+
if client is not None:
|
|
300
|
+
if any(value is not None for value in (dsn, user, password, env_file_path, env_file_encoding)):
|
|
301
|
+
raise ValueError("client cannot be combined with dsn, user, password, or .env settings.")
|
|
302
|
+
return _Client(client=client)
|
|
303
|
+
settings = load_settings(
|
|
304
|
+
OracleSettings,
|
|
305
|
+
env_prefix="ORACLE_",
|
|
306
|
+
dsn=dsn,
|
|
307
|
+
user=user,
|
|
308
|
+
password=password,
|
|
309
|
+
env_file_path=env_file_path,
|
|
310
|
+
env_file_encoding=env_file_encoding,
|
|
311
|
+
)
|
|
312
|
+
return _Client(dsn=settings.get("dsn"), user=settings.get("user"), password=settings.get("password"))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _scalar_field(definition: VectorStoreCollectionDefinition, name: str) -> VectorStoreField:
|
|
316
|
+
if "." in name:
|
|
317
|
+
raise NotImplementedError("Oracle filters do not support nested field paths.")
|
|
318
|
+
field = definition.try_get_field(name)
|
|
319
|
+
if field is None:
|
|
320
|
+
raise ValueError(f"Unknown Oracle field '{name}'.")
|
|
321
|
+
if field.field_type == "vector":
|
|
322
|
+
raise NotImplementedError("Oracle cannot filter or order vector columns.")
|
|
323
|
+
return field
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class _FilterCompiler:
|
|
327
|
+
"""Compile core-validated scalar filters to two-valued, bound Oracle SQL."""
|
|
328
|
+
|
|
329
|
+
def __init__(self, definition: VectorStoreCollectionDefinition) -> None:
|
|
330
|
+
self.definition = definition
|
|
331
|
+
self.binds: dict[str, Any] = {}
|
|
332
|
+
|
|
333
|
+
def compile(self, expression: FilterExpression) -> tuple[str, dict[str, Any]]:
|
|
334
|
+
"""Compile an expression and its bind values without retaining caller state."""
|
|
335
|
+
self.binds = {}
|
|
336
|
+
return self._condition(expression), self.binds
|
|
337
|
+
|
|
338
|
+
def _bind(self, value: Any) -> str:
|
|
339
|
+
name = f"f{len(self.binds)}"
|
|
340
|
+
self.binds[name] = value
|
|
341
|
+
return f":{name}"
|
|
342
|
+
|
|
343
|
+
def _equality(self, field: VectorStoreField, value: Any) -> str:
|
|
344
|
+
column = _quote_identifier(field.storage_name or field.name)
|
|
345
|
+
if value is None:
|
|
346
|
+
return f"{column} IS NULL"
|
|
347
|
+
kind = field.type_
|
|
348
|
+
if kind in ("int", "float") and (type(value) in (int, float) or isinstance(value, Decimal)):
|
|
349
|
+
if type(value) is float and not math.isfinite(value):
|
|
350
|
+
raise ValueError("Numeric filter values must be finite.")
|
|
351
|
+
if isinstance(value, Decimal) and not value.is_finite():
|
|
352
|
+
raise ValueError("Numeric filter values must be finite.")
|
|
353
|
+
if kind == "int":
|
|
354
|
+
if not -(2**63) <= value < 2**63 or value != int(value):
|
|
355
|
+
return "1=0"
|
|
356
|
+
adapted = int(value)
|
|
357
|
+
else:
|
|
358
|
+
try:
|
|
359
|
+
adapted = float(value)
|
|
360
|
+
except OverflowError:
|
|
361
|
+
return "1=0"
|
|
362
|
+
if not math.isfinite(adapted):
|
|
363
|
+
return "1=0"
|
|
364
|
+
if isinstance(value, Decimal) and Decimal.from_float(adapted) != value:
|
|
365
|
+
return "1=0"
|
|
366
|
+
if type(value) is int:
|
|
367
|
+
numerator, denominator = adapted.as_integer_ratio()
|
|
368
|
+
if numerator != value * denominator:
|
|
369
|
+
return "1=0"
|
|
370
|
+
elif kind == "UUID":
|
|
371
|
+
if not isinstance(value, UUID | str):
|
|
372
|
+
return "1=0"
|
|
373
|
+
try:
|
|
374
|
+
adapted = _validate_scalar(field, value)
|
|
375
|
+
except ValueError:
|
|
376
|
+
return "1=0"
|
|
377
|
+
elif (
|
|
378
|
+
(kind == "bool" and type(value) is not bool)
|
|
379
|
+
or (kind != "bool" and isinstance(value, bool))
|
|
380
|
+
or (kind == "str" and not isinstance(value, str))
|
|
381
|
+
or (kind in ("int", "float") and type(value) not in (int, float, Decimal))
|
|
382
|
+
):
|
|
383
|
+
return "1=0"
|
|
384
|
+
else:
|
|
385
|
+
adapted = _validate_scalar(field, value)
|
|
386
|
+
return f"({column} IS NOT NULL AND {column} = {self._bind(adapted)})"
|
|
387
|
+
|
|
388
|
+
def _condition(self, expression: FilterExpression) -> str:
|
|
389
|
+
if isinstance(expression, FilterGroup):
|
|
390
|
+
parts = [self._condition(child) for child in expression.filters]
|
|
391
|
+
if expression.operator == "not":
|
|
392
|
+
return f"(NOT ({parts[0]}))"
|
|
393
|
+
joiner = " AND " if expression.operator == "and" else " OR "
|
|
394
|
+
return f"({joiner.join(parts)})"
|
|
395
|
+
field = _scalar_field(self.definition, expression.field_name)
|
|
396
|
+
column = _quote_identifier(field.storage_name or field.name)
|
|
397
|
+
op, value = expression.operator, expression.value
|
|
398
|
+
if op == "exists":
|
|
399
|
+
return "1=1"
|
|
400
|
+
if op == "is_null":
|
|
401
|
+
return f"{column} IS NULL"
|
|
402
|
+
if op == "is_not_null":
|
|
403
|
+
return f"{column} IS NOT NULL"
|
|
404
|
+
if op in ("eq", "ne"):
|
|
405
|
+
equality = self._equality(field, value)
|
|
406
|
+
return equality if op == "eq" else f"(NOT ({equality}))"
|
|
407
|
+
if op in ("in", "not_in"):
|
|
408
|
+
parts = [self._equality(field, item) for item in cast(Collection[Any], value)]
|
|
409
|
+
membership = f"({' OR '.join(parts)})" if parts else "1=0"
|
|
410
|
+
if op == "not_in":
|
|
411
|
+
membership = f"(NOT ({membership}))"
|
|
412
|
+
return f"({column} IS NOT NULL AND {membership})"
|
|
413
|
+
if op in _ORDER_OPERATORS or op == "between":
|
|
414
|
+
if field.type_ not in ("int", "float"):
|
|
415
|
+
raise NotImplementedError("Oracle ordered filters support only numeric fields.")
|
|
416
|
+
operands = cast(Sequence[Any], value) if op == "between" else [value]
|
|
417
|
+
adapted: list[Any] = []
|
|
418
|
+
for item in operands:
|
|
419
|
+
if item is None or isinstance(item, bool) or type(item) not in (int, float):
|
|
420
|
+
raise TypeError("Ordered filter operands must be non-null numbers, not booleans.")
|
|
421
|
+
if type(item) is float and not math.isfinite(item):
|
|
422
|
+
raise ValueError("Ordered filter numbers must be finite.")
|
|
423
|
+
if field.type_ == "int" and type(item) is float and not item.is_integer():
|
|
424
|
+
raise NotImplementedError("Oracle integer range filters require integer operands.")
|
|
425
|
+
adapted.append(_validate_scalar(field, int(item) if field.type_ == "int" else item))
|
|
426
|
+
comparison = (
|
|
427
|
+
f"{column} BETWEEN {self._bind(adapted[0])} AND {self._bind(adapted[1])}"
|
|
428
|
+
if op == "between"
|
|
429
|
+
else f"{column} {_ORDER_OPERATORS[op]} {self._bind(adapted[0])}"
|
|
430
|
+
)
|
|
431
|
+
return f"({column} IS NOT NULL AND {comparison})"
|
|
432
|
+
if op in ("starts_with", "ends_with", "contains_text"):
|
|
433
|
+
if field.type_ != "str":
|
|
434
|
+
raise TypeError("Oracle text filtering requires a string column.")
|
|
435
|
+
if not isinstance(value, str):
|
|
436
|
+
raise TypeError("Oracle text filtering requires a string operand.")
|
|
437
|
+
escaped = value.replace("!", "!!").replace("%", "!%").replace("_", "!_")
|
|
438
|
+
pattern = ("%" if op != "starts_with" else "") + escaped + ("%" if op != "ends_with" else "")
|
|
439
|
+
return f"({column} IS NOT NULL AND {column} LIKE {self._bind(pattern)} ESCAPE '!')"
|
|
440
|
+
raise NotImplementedError(f"Unsupported Oracle filter operator '{op}'.")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class OracleCollection(
|
|
444
|
+
BaseVectorCollection[KeyT, ModelT],
|
|
445
|
+
BaseVectorSearch[KeyT, ModelT],
|
|
446
|
+
Generic[KeyT, ModelT],
|
|
447
|
+
):
|
|
448
|
+
"""Store typed records and search Oracle Database 23ai native VECTOR columns."""
|
|
449
|
+
|
|
450
|
+
supported_key_types: ClassVar[set[str] | None] = {"str", "int", "UUID"}
|
|
451
|
+
supported_vector_types: ClassVar[set[str] | None] = {"float", "float32", "float64", "int8"}
|
|
452
|
+
supported_search_types: ClassVar[set[SearchType]] = {"vector"}
|
|
453
|
+
|
|
454
|
+
def __init__(
|
|
455
|
+
self,
|
|
456
|
+
record_type: type[ModelT],
|
|
457
|
+
*,
|
|
458
|
+
definition: VectorStoreCollectionDefinition | None = None,
|
|
459
|
+
collection_name: str | None = None,
|
|
460
|
+
embedding_generator: EmbeddingClient | None = None,
|
|
461
|
+
dsn: str | None = None,
|
|
462
|
+
user: str | None = None,
|
|
463
|
+
password: str | SecretString | None = None,
|
|
464
|
+
client: OracleClient | None = None,
|
|
465
|
+
env_file_path: str | None = None,
|
|
466
|
+
env_file_encoding: str | None = None,
|
|
467
|
+
_shared_client: _Client | None = None,
|
|
468
|
+
) -> None:
|
|
469
|
+
"""Initialize a collection with explicit, selected .env, or environment credentials.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
record_type: Registered application model, or dict with an explicit definition.
|
|
473
|
+
definition: Explicit dictionary model definition.
|
|
474
|
+
collection_name: Override for the model's table name in the current Oracle schema.
|
|
475
|
+
embedding_generator: Optional local embedding generator.
|
|
476
|
+
dsn: Oracle connect string; defaults to ``ORACLE_DSN``.
|
|
477
|
+
user: Oracle database user; defaults to ``ORACLE_USER``.
|
|
478
|
+
password: Database password or AF SecretString; defaults to ``ORACLE_PASSWORD``.
|
|
479
|
+
client: Borrowed async connection or pool in place of connection settings.
|
|
480
|
+
env_file_path: Optional selected .env file; no implicit discovery.
|
|
481
|
+
env_file_encoding: Encoding of the selected .env file.
|
|
482
|
+
"""
|
|
483
|
+
if _shared_client is not None and any(
|
|
484
|
+
item is not None for item in (dsn, user, password, client, env_file_path, env_file_encoding)
|
|
485
|
+
):
|
|
486
|
+
raise ValueError("Shared Oracle clients cannot be combined with connection settings.")
|
|
487
|
+
super().__init__(
|
|
488
|
+
record_type,
|
|
489
|
+
definition=definition,
|
|
490
|
+
collection_name=collection_name,
|
|
491
|
+
embedding_generator=embedding_generator,
|
|
492
|
+
managed_client=_shared_client is None and client is None,
|
|
493
|
+
)
|
|
494
|
+
self._table = _quote_identifier(self.collection_name)
|
|
495
|
+
self._fields = tuple(self.definition.fields)
|
|
496
|
+
self._client = _shared_client or _create_client(
|
|
497
|
+
dsn=dsn,
|
|
498
|
+
user=user,
|
|
499
|
+
password=password,
|
|
500
|
+
client=client,
|
|
501
|
+
env_file_path=env_file_path,
|
|
502
|
+
env_file_encoding=env_file_encoding,
|
|
503
|
+
)
|
|
504
|
+
self._shared_client = _shared_client is not None
|
|
505
|
+
|
|
506
|
+
def _validate_data_model(self) -> None:
|
|
507
|
+
super()._validate_data_model()
|
|
508
|
+
if not self.definition.vector_fields:
|
|
509
|
+
raise ValueError("Oracle collections require a vector field.")
|
|
510
|
+
for field in self.definition.fields:
|
|
511
|
+
_quote_identifier(field.storage_name or field.name)
|
|
512
|
+
if field.provider_annotations:
|
|
513
|
+
raise NotImplementedError("Oracle provider annotations are not supported.")
|
|
514
|
+
if field.is_full_text_indexed or field.is_indexed:
|
|
515
|
+
raise NotImplementedError("Oracle data and full-text indexes are not supported.")
|
|
516
|
+
if field.is_auto_generated:
|
|
517
|
+
raise NotImplementedError("Oracle requires application-provided keys.")
|
|
518
|
+
if field.field_type == "vector":
|
|
519
|
+
if field.index_kind not in ("default", "flat"):
|
|
520
|
+
raise NotImplementedError(f"Oracle index kind '{field.index_kind}' is not supported.")
|
|
521
|
+
if field.distance_function not in _METRICS:
|
|
522
|
+
raise NotImplementedError(f"Oracle distance function '{field.distance_function}' is not supported.")
|
|
523
|
+
elif field.field_type == "key" and field.type_ not in ("str", "int", "UUID"):
|
|
524
|
+
raise NotImplementedError(f"Oracle key type '{field.type_}' is not supported.")
|
|
525
|
+
elif field.field_type == "data" and field.type_ not in ("str", "int", "float", "bool"):
|
|
526
|
+
raise NotImplementedError(f"Oracle data type '{field.type_}' is not supported.")
|
|
527
|
+
_column_type(field)
|
|
528
|
+
|
|
529
|
+
async def __aenter__(self) -> Self:
|
|
530
|
+
"""Enter the collection context without connecting until first use."""
|
|
531
|
+
return self
|
|
532
|
+
|
|
533
|
+
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
|
534
|
+
"""Close only a collection-owned pool."""
|
|
535
|
+
await self.close()
|
|
536
|
+
|
|
537
|
+
async def close(self) -> None:
|
|
538
|
+
"""Release this collection's resources, not a borrowed or store-shared client."""
|
|
539
|
+
if not self._shared_client:
|
|
540
|
+
await self._client.close()
|
|
541
|
+
|
|
542
|
+
async def collection_exists(self, *, operation_options: Mapping[str, Any] | None = None) -> bool:
|
|
543
|
+
"""Check whether the table exists in the current user's schema."""
|
|
544
|
+
_validate_operation_options(operation_options)
|
|
545
|
+
async with self._client.connection() as connection, cast(_OracleCursor, connection.cursor()) as cursor:
|
|
546
|
+
await cursor.execute("SELECT 1 FROM USER_TABLES WHERE TABLE_NAME = :name", {"name": self.collection_name})
|
|
547
|
+
return await cursor.fetchone() is not None
|
|
548
|
+
|
|
549
|
+
async def ensure_collection_exists(self, *, operation_options: Mapping[str, Any] | None = None) -> None:
|
|
550
|
+
"""Create this table if absent; do not migrate existing tables or create indexes."""
|
|
551
|
+
_validate_operation_options(operation_options)
|
|
552
|
+
columns: list[str] = []
|
|
553
|
+
for field in self._fields:
|
|
554
|
+
column = f"{_quote_identifier(field.storage_name or field.name)} {_column_type(field)}"
|
|
555
|
+
columns.append(column + (" PRIMARY KEY" if field.field_type == "key" else ""))
|
|
556
|
+
async with self._client.connection() as connection, cast(_OracleCursor, connection.cursor()) as cursor:
|
|
557
|
+
await cursor.execute("SELECT 1 FROM USER_TABLES WHERE TABLE_NAME = :name", {"name": self.collection_name})
|
|
558
|
+
if await cursor.fetchone() is not None:
|
|
559
|
+
return
|
|
560
|
+
try:
|
|
561
|
+
await cursor.execute(f"CREATE TABLE {self._table} ({', '.join(columns)})")
|
|
562
|
+
except oracledb.DatabaseError as exc:
|
|
563
|
+
if _oracle_error_code(exc) != 955:
|
|
564
|
+
raise
|
|
565
|
+
await cursor.execute(
|
|
566
|
+
"SELECT 1 FROM USER_TABLES WHERE TABLE_NAME = :name", {"name": self.collection_name}
|
|
567
|
+
)
|
|
568
|
+
if await cursor.fetchone() is None:
|
|
569
|
+
raise
|
|
570
|
+
|
|
571
|
+
async def ensure_collection_deleted(self, *, operation_options: Mapping[str, Any] | None = None) -> None:
|
|
572
|
+
"""Drop only this table, without modifying other database objects."""
|
|
573
|
+
_validate_operation_options(operation_options)
|
|
574
|
+
if await self.collection_exists():
|
|
575
|
+
await _drop_table(self._client, self.collection_name)
|
|
576
|
+
|
|
577
|
+
def _deserialize_store_models_to_dicts(
|
|
578
|
+
self,
|
|
579
|
+
records: Sequence[Any],
|
|
580
|
+
*,
|
|
581
|
+
context: Mapping[str, Any] | None = None,
|
|
582
|
+
) -> Sequence[dict[str, Any]]:
|
|
583
|
+
result = super()._deserialize_store_models_to_dicts(records, context=context)
|
|
584
|
+
for record in result:
|
|
585
|
+
for field in self._fields:
|
|
586
|
+
name = field.storage_name or field.name
|
|
587
|
+
if field.field_type == "vector" and record.get(name) is not None:
|
|
588
|
+
if not isinstance(record[name], array):
|
|
589
|
+
raise IntegrationInvalidResponseException(f"Oracle returned an invalid vector for '{name}'.")
|
|
590
|
+
record[name] = record[name].tolist()
|
|
591
|
+
return result
|
|
592
|
+
|
|
593
|
+
def _prepare_filter(self, filter: FilterExpression | None) -> tuple[str, dict[str, Any]]:
|
|
594
|
+
return ("1=1", {}) if filter is None else _FilterCompiler(self.definition).compile(filter)
|
|
595
|
+
|
|
596
|
+
def _prepare_order_by(self, order_by: Mapping[str, bool] | None) -> str:
|
|
597
|
+
parts: list[str] = []
|
|
598
|
+
ordered_fields: set[str] = set()
|
|
599
|
+
for name, ascending in (order_by or {}).items():
|
|
600
|
+
if type(ascending) is not bool:
|
|
601
|
+
raise TypeError("Oracle order directions must be booleans.")
|
|
602
|
+
field = _scalar_field(self.definition, name)
|
|
603
|
+
parts.append(
|
|
604
|
+
f"{_quote_identifier(field.storage_name or field.name)} {'ASC' if ascending else 'DESC'} NULLS LAST"
|
|
605
|
+
)
|
|
606
|
+
ordered_fields.add(field.name)
|
|
607
|
+
if self.definition.key_name not in ordered_fields:
|
|
608
|
+
parts.append(f"{_quote_identifier(self.definition.key_field_storage_name)} ASC")
|
|
609
|
+
return ", ".join(parts)
|
|
610
|
+
|
|
611
|
+
def _prepare_key(self, key: Any) -> Any:
|
|
612
|
+
return _validate_scalar(self.definition.key_field, key)
|
|
613
|
+
|
|
614
|
+
def _decode_row(self, fields: Sequence[VectorStoreField], row: Sequence[Any]) -> dict[str, Any]:
|
|
615
|
+
if len(row) != len(fields):
|
|
616
|
+
raise IntegrationInvalidResponseException("Oracle returned an unexpected number of columns.")
|
|
617
|
+
result: dict[str, Any] = {}
|
|
618
|
+
for field, value in zip(fields, row, strict=True):
|
|
619
|
+
if field.field_type == "vector":
|
|
620
|
+
result[field.storage_name or field.name] = value
|
|
621
|
+
continue
|
|
622
|
+
if value is not None and field.type_ == "UUID":
|
|
623
|
+
if not isinstance(value, UUID | str):
|
|
624
|
+
raise IntegrationInvalidResponseException(f"Oracle returned an invalid UUID for '{field.name}'.")
|
|
625
|
+
try:
|
|
626
|
+
value = UUID(str(value))
|
|
627
|
+
except ValueError as exc:
|
|
628
|
+
raise IntegrationInvalidResponseException(
|
|
629
|
+
f"Oracle returned an invalid UUID for '{field.name}'."
|
|
630
|
+
) from exc
|
|
631
|
+
elif value is not None and field.type_ == "int":
|
|
632
|
+
if (isinstance(value, Decimal) and value.is_finite() and value == int(value)) or (
|
|
633
|
+
type(value) is float and math.isfinite(value) and abs(value) <= 2**53 and value.is_integer()
|
|
634
|
+
):
|
|
635
|
+
value = int(value)
|
|
636
|
+
if type(value) is not int or not -(2**63) <= value < 2**63:
|
|
637
|
+
raise IntegrationInvalidResponseException(f"Oracle returned an invalid integer for '{field.name}'.")
|
|
638
|
+
elif value is not None and field.type_ == "float":
|
|
639
|
+
if isinstance(value, bool) or not isinstance(value, int | float | Decimal):
|
|
640
|
+
raise IntegrationInvalidResponseException(f"Oracle returned an invalid float for '{field.name}'.")
|
|
641
|
+
try:
|
|
642
|
+
value = float(value)
|
|
643
|
+
except OverflowError as exc:
|
|
644
|
+
raise IntegrationInvalidResponseException(
|
|
645
|
+
f"Oracle returned an invalid float for '{field.name}'."
|
|
646
|
+
) from exc
|
|
647
|
+
if not math.isfinite(value):
|
|
648
|
+
raise IntegrationInvalidResponseException(f"Oracle returned an invalid float for '{field.name}'.")
|
|
649
|
+
elif value is not None and field.type_ == "bool":
|
|
650
|
+
if value not in (0, 1):
|
|
651
|
+
raise IntegrationInvalidResponseException(f"Oracle returned an invalid boolean for '{field.name}'.")
|
|
652
|
+
value = bool(value)
|
|
653
|
+
result[field.storage_name or field.name] = value
|
|
654
|
+
return result
|
|
655
|
+
|
|
656
|
+
async def _inner_upsert(
|
|
657
|
+
self, records: Sequence[Any], *, operation_options: Mapping[str, Any] | None = None
|
|
658
|
+
) -> Sequence[KeyT]:
|
|
659
|
+
_validate_operation_options(operation_options)
|
|
660
|
+
self._client.ensure_open()
|
|
661
|
+
binds: list[dict[str, Any]] = []
|
|
662
|
+
keys: list[KeyT] = []
|
|
663
|
+
key_index = self._fields.index(self.definition.key_field)
|
|
664
|
+
for record in records:
|
|
665
|
+
row: dict[str, Any] = {}
|
|
666
|
+
for index, field in enumerate(self._fields):
|
|
667
|
+
name = field.storage_name or field.name
|
|
668
|
+
row[f"p{index}"] = (
|
|
669
|
+
_prepare_vector(field, record[name])
|
|
670
|
+
if field.field_type == "vector"
|
|
671
|
+
else _validate_scalar(field, record[name])
|
|
672
|
+
)
|
|
673
|
+
key = row[f"p{key_index}"]
|
|
674
|
+
keys.append(cast(KeyT, UUID(key) if self.definition.key_field.type_ == "UUID" else key))
|
|
675
|
+
binds.append(row)
|
|
676
|
+
if not binds:
|
|
677
|
+
return []
|
|
678
|
+
key_column = _quote_identifier(self.definition.key_field_storage_name)
|
|
679
|
+
updates = [
|
|
680
|
+
f"target.{_quote_identifier(field.storage_name or field.name)} = :p{index}"
|
|
681
|
+
for index, field in enumerate(self._fields)
|
|
682
|
+
if index != key_index
|
|
683
|
+
]
|
|
684
|
+
columns = ", ".join(_quote_identifier(field.storage_name or field.name) for field in self._fields)
|
|
685
|
+
placeholders = ", ".join(f":p{index}" for index in range(len(self._fields)))
|
|
686
|
+
# SQL interpolates only quoted identifiers and fixed fragments; data remains bound.
|
|
687
|
+
statement = (
|
|
688
|
+
f"MERGE INTO {self._table} target USING DUAL " # ruff: ignore[hardcoded-sql-expression] # nosec B608
|
|
689
|
+
f"ON (target.{key_column} = :p{key_index}) "
|
|
690
|
+
f"WHEN MATCHED THEN UPDATE SET {', '.join(updates)} "
|
|
691
|
+
f"WHEN NOT MATCHED THEN INSERT ({columns}) VALUES ({placeholders})"
|
|
692
|
+
)
|
|
693
|
+
async with (
|
|
694
|
+
self._client.connection(write=True) as connection,
|
|
695
|
+
cast(_OracleCursor, connection.cursor()) as cursor,
|
|
696
|
+
):
|
|
697
|
+
cursor.setinputsizes(**{f"p{index}": _bind_type(field) for index, field in enumerate(self._fields)})
|
|
698
|
+
for start in range(0, len(binds), _BATCH_SIZE):
|
|
699
|
+
await cursor.executemany(statement, binds[start : start + _BATCH_SIZE])
|
|
700
|
+
return keys
|
|
701
|
+
|
|
702
|
+
async def _inner_get(
|
|
703
|
+
self,
|
|
704
|
+
*,
|
|
705
|
+
keys: Sequence[KeyT] | None = None,
|
|
706
|
+
filter: FilterExpression | None = None,
|
|
707
|
+
top: int = 10,
|
|
708
|
+
skip: int = 0,
|
|
709
|
+
order_by: Mapping[str, bool] | None = None,
|
|
710
|
+
include_vectors: bool = False,
|
|
711
|
+
operation_options: Mapping[str, Any] | None = None,
|
|
712
|
+
) -> Sequence[Any]:
|
|
713
|
+
_validate_operation_options(operation_options)
|
|
714
|
+
self._client.ensure_open()
|
|
715
|
+
fields = tuple(field for field in self._fields if include_vectors or field.field_type != "vector")
|
|
716
|
+
columns = ", ".join(_quote_identifier(field.storage_name or field.name) for field in fields)
|
|
717
|
+
if keys is not None:
|
|
718
|
+
if order_by or skip or top != 10:
|
|
719
|
+
raise ValueError("Oracle key lookup cannot be combined with order_by, top, or skip.")
|
|
720
|
+
adapted = [self._prepare_key(key) for key in keys]
|
|
721
|
+
if not adapted:
|
|
722
|
+
return []
|
|
723
|
+
found: dict[Any, dict[str, Any]] = {}
|
|
724
|
+
key_column = _quote_identifier(self.definition.key_field_storage_name)
|
|
725
|
+
async with self._client.connection() as connection, cast(_OracleCursor, connection.cursor()) as cursor:
|
|
726
|
+
for start in range(0, len(adapted), _BATCH_SIZE):
|
|
727
|
+
chunk = adapted[start : start + _BATCH_SIZE]
|
|
728
|
+
binds = {f"k{index}": key for index, key in enumerate(chunk)}
|
|
729
|
+
placeholders = ", ".join(f":{name}" for name in binds)
|
|
730
|
+
statement = (
|
|
731
|
+
f"SELECT {columns} FROM {self._table} " # ruff: ignore[hardcoded-sql-expression] # nosec B608
|
|
732
|
+
f"WHERE {key_column} IN ({placeholders})"
|
|
733
|
+
)
|
|
734
|
+
await cursor.execute(statement, binds)
|
|
735
|
+
for row in await cursor.fetchall():
|
|
736
|
+
decoded = self._decode_row(fields, row)
|
|
737
|
+
found[self._prepare_key(decoded[self.definition.key_field_storage_name])] = decoded
|
|
738
|
+
return [found[key] for key in adapted if key in found]
|
|
739
|
+
where, binds = self._prepare_filter(filter)
|
|
740
|
+
order = self._prepare_order_by(order_by)
|
|
741
|
+
binds.update(top=top, skip=skip)
|
|
742
|
+
async with self._client.connection() as connection, cast(_OracleCursor, connection.cursor()) as cursor:
|
|
743
|
+
statement = (
|
|
744
|
+
f"SELECT {columns} FROM {self._table} " # ruff: ignore[hardcoded-sql-expression] # nosec B608
|
|
745
|
+
f"WHERE {where} ORDER BY {order} "
|
|
746
|
+
"OFFSET :skip ROWS FETCH NEXT :top ROWS ONLY"
|
|
747
|
+
)
|
|
748
|
+
await cursor.execute(statement, binds)
|
|
749
|
+
return [self._decode_row(fields, row) for row in await cursor.fetchall()]
|
|
750
|
+
|
|
751
|
+
async def _inner_delete(self, keys: Sequence[KeyT], *, operation_options: Mapping[str, Any] | None = None) -> None:
|
|
752
|
+
_validate_operation_options(operation_options)
|
|
753
|
+
self._client.ensure_open()
|
|
754
|
+
adapted = [self._prepare_key(key) for key in keys]
|
|
755
|
+
if not adapted:
|
|
756
|
+
return
|
|
757
|
+
key_column = _quote_identifier(self.definition.key_field_storage_name)
|
|
758
|
+
async with (
|
|
759
|
+
self._client.connection(write=True) as connection,
|
|
760
|
+
cast(_OracleCursor, connection.cursor()) as cursor,
|
|
761
|
+
):
|
|
762
|
+
for start in range(0, len(adapted), _BATCH_SIZE):
|
|
763
|
+
chunk = adapted[start : start + _BATCH_SIZE]
|
|
764
|
+
binds = {f"k{index}": key for index, key in enumerate(chunk)}
|
|
765
|
+
placeholders = ", ".join(f":{name}" for name in binds)
|
|
766
|
+
statement = (
|
|
767
|
+
f"DELETE FROM {self._table} " # ruff: ignore[hardcoded-sql-expression] # nosec B608
|
|
768
|
+
f"WHERE {key_column} IN ({placeholders})"
|
|
769
|
+
)
|
|
770
|
+
await cursor.execute(statement, binds)
|
|
771
|
+
|
|
772
|
+
async def _inner_search(
|
|
773
|
+
self,
|
|
774
|
+
*,
|
|
775
|
+
search_type: SearchType,
|
|
776
|
+
filter: FilterExpression | None = None,
|
|
777
|
+
values: Any | None = None,
|
|
778
|
+
vector: Vector | None = None,
|
|
779
|
+
top: int = 3,
|
|
780
|
+
skip: int = 0,
|
|
781
|
+
include_vectors: bool = False,
|
|
782
|
+
vector_property_name: str | None = None,
|
|
783
|
+
additional_property_name: str | None = None,
|
|
784
|
+
score_threshold: float | None = None,
|
|
785
|
+
operation_options: Mapping[str, Any] | None = None,
|
|
786
|
+
) -> SearchResults[Any]:
|
|
787
|
+
_validate_operation_options(operation_options)
|
|
788
|
+
if search_type != "vector" or additional_property_name is not None:
|
|
789
|
+
raise NotImplementedError("Oracle supports dense vector search only, not keyword-hybrid search.")
|
|
790
|
+
if vector is None:
|
|
791
|
+
raise NotImplementedError(
|
|
792
|
+
"Oracle has no server-side vectorization; supply a vector or embedding generator."
|
|
793
|
+
)
|
|
794
|
+
self._client.ensure_open()
|
|
795
|
+
field = self.definition.try_get_vector_field(vector_property_name)
|
|
796
|
+
if field is None:
|
|
797
|
+
raise ValueError("Select a vector_property_name from the collection definition.")
|
|
798
|
+
prepared = _prepare_vector(field, vector)
|
|
799
|
+
column = _quote_identifier(field.storage_name or field.name)
|
|
800
|
+
metric = _METRICS[field.distance_function or "DEFAULT"]
|
|
801
|
+
distance = f"VECTOR_DISTANCE({column}, :query_vector, {metric})"
|
|
802
|
+
score = (
|
|
803
|
+
f"(1 - {distance})"
|
|
804
|
+
if field.distance_function == "cosine_similarity"
|
|
805
|
+
else (f"(-{distance})" if field.distance_function == "dot_prod" else distance)
|
|
806
|
+
)
|
|
807
|
+
where, binds = self._prepare_filter(filter)
|
|
808
|
+
binds["query_vector"] = prepared
|
|
809
|
+
predicate = f"({where}) AND {column} IS NOT NULL"
|
|
810
|
+
if score_threshold is not None:
|
|
811
|
+
if type(score_threshold) not in (int, float):
|
|
812
|
+
raise ValueError("Oracle score_threshold must be a finite number.")
|
|
813
|
+
try:
|
|
814
|
+
score_threshold = float(score_threshold)
|
|
815
|
+
except OverflowError as exc:
|
|
816
|
+
raise ValueError("Oracle score_threshold must be a finite number.") from exc
|
|
817
|
+
if not math.isfinite(score_threshold):
|
|
818
|
+
raise ValueError("Oracle score_threshold must be a finite number.")
|
|
819
|
+
cutoff = (
|
|
820
|
+
1 - score_threshold
|
|
821
|
+
if field.distance_function == "cosine_similarity"
|
|
822
|
+
else -score_threshold
|
|
823
|
+
if field.distance_function == "dot_prod"
|
|
824
|
+
else score_threshold
|
|
825
|
+
)
|
|
826
|
+
binds["threshold"] = cutoff
|
|
827
|
+
predicate += f" AND {distance} <= :threshold"
|
|
828
|
+
binds.update(top=top, skip=skip)
|
|
829
|
+
fields = tuple(item for item in self._fields if include_vectors or item.field_type != "vector")
|
|
830
|
+
columns = ", ".join(_quote_identifier(item.storage_name or item.name) for item in fields)
|
|
831
|
+
statement = (
|
|
832
|
+
f"SELECT {columns}, {score} FROM {self._table} " # ruff: ignore[hardcoded-sql-expression] # nosec B608
|
|
833
|
+
f"WHERE {predicate} "
|
|
834
|
+
f"ORDER BY {distance} ASC, {_quote_identifier(self.definition.key_field_storage_name)} ASC "
|
|
835
|
+
"OFFSET :skip ROWS FETCH NEXT :top ROWS ONLY"
|
|
836
|
+
)
|
|
837
|
+
async with self._client.connection() as connection, cast(_OracleCursor, connection.cursor()) as cursor:
|
|
838
|
+
cursor.setinputsizes(query_vector=_bind_type(field))
|
|
839
|
+
await cursor.execute(statement, binds)
|
|
840
|
+
rows = await cursor.fetchall()
|
|
841
|
+
results: list[dict[str, Any]] = []
|
|
842
|
+
for row in rows:
|
|
843
|
+
record = self._decode_row(fields, row[:-1])
|
|
844
|
+
if row[-1] is None:
|
|
845
|
+
raise IntegrationInvalidResponseException("Oracle returned a null vector distance.")
|
|
846
|
+
score_value = float(row[-1])
|
|
847
|
+
if not math.isfinite(score_value):
|
|
848
|
+
raise IntegrationInvalidResponseException("Oracle returned a nonfinite vector distance.")
|
|
849
|
+
results.append({"record": record, "score": score_value})
|
|
850
|
+
return SearchResults(results, metadata={"distance_function": field.distance_function or "DEFAULT"})
|
|
851
|
+
|
|
852
|
+
def _get_record_from_result(self, result: Any) -> Any:
|
|
853
|
+
return result["record"]
|
|
854
|
+
|
|
855
|
+
def _get_score_from_result(self, result: Any) -> float | None:
|
|
856
|
+
return result["score"]
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
async def _drop_table(client: _Client, name: str) -> None:
|
|
860
|
+
async with client.connection() as connection, cast(_OracleCursor, connection.cursor()) as cursor:
|
|
861
|
+
try:
|
|
862
|
+
await cursor.execute(f"DROP TABLE {_quote_identifier(name)} PURGE")
|
|
863
|
+
except oracledb.DatabaseError as exc:
|
|
864
|
+
if _oracle_error_code(exc) != 942:
|
|
865
|
+
raise
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class OracleStore(BaseVectorStore):
|
|
869
|
+
"""Create Oracle vector collections sharing one async pool or connection."""
|
|
870
|
+
|
|
871
|
+
def __init__(
|
|
872
|
+
self,
|
|
873
|
+
*,
|
|
874
|
+
dsn: str | None = None,
|
|
875
|
+
user: str | None = None,
|
|
876
|
+
password: str | SecretString | None = None,
|
|
877
|
+
client: OracleClient | None = None,
|
|
878
|
+
embedding_generator: EmbeddingClient | None = None,
|
|
879
|
+
env_file_path: str | None = None,
|
|
880
|
+
env_file_encoding: str | None = None,
|
|
881
|
+
) -> None:
|
|
882
|
+
"""Initialize a store without opening its connector-owned pool.
|
|
883
|
+
|
|
884
|
+
Args:
|
|
885
|
+
dsn: Oracle connect string; defaults to ``ORACLE_DSN``.
|
|
886
|
+
user: Oracle database user; defaults to ``ORACLE_USER``.
|
|
887
|
+
password: Database password or AF SecretString; defaults to ``ORACLE_PASSWORD``.
|
|
888
|
+
client: Borrowed async connection or pool in place of connection settings.
|
|
889
|
+
embedding_generator: Default local embedding client for child collections.
|
|
890
|
+
env_file_path: Optional selected .env file; no implicit discovery.
|
|
891
|
+
env_file_encoding: Encoding of the selected .env file.
|
|
892
|
+
"""
|
|
893
|
+
super().__init__(embedding_generator=embedding_generator, managed_client=client is None)
|
|
894
|
+
self._client = _create_client(
|
|
895
|
+
dsn=dsn,
|
|
896
|
+
user=user,
|
|
897
|
+
password=password,
|
|
898
|
+
client=client,
|
|
899
|
+
env_file_path=env_file_path,
|
|
900
|
+
env_file_encoding=env_file_encoding,
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
def get_collection(
|
|
904
|
+
self,
|
|
905
|
+
record_type: type[ModelT],
|
|
906
|
+
*,
|
|
907
|
+
definition: VectorStoreCollectionDefinition | None = None,
|
|
908
|
+
collection_name: str | None = None,
|
|
909
|
+
embedding_generator: EmbeddingClient | None = None,
|
|
910
|
+
) -> OracleCollection[Any, ModelT]:
|
|
911
|
+
"""Create a collection borrowing this store's connection lifecycle."""
|
|
912
|
+
return OracleCollection(
|
|
913
|
+
record_type,
|
|
914
|
+
definition=definition,
|
|
915
|
+
collection_name=collection_name,
|
|
916
|
+
embedding_generator=embedding_generator if embedding_generator is not None else self.embedding_generator,
|
|
917
|
+
_shared_client=self._client,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
async def list_collection_names(self, *, operation_options: Mapping[str, Any] | None = None) -> Sequence[str]:
|
|
921
|
+
"""List base table names in the current Oracle user schema."""
|
|
922
|
+
_validate_operation_options(operation_options)
|
|
923
|
+
async with self._client.connection() as connection, cast(_OracleCursor, connection.cursor()) as cursor:
|
|
924
|
+
await cursor.execute("SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME")
|
|
925
|
+
names: list[str] = []
|
|
926
|
+
for row in await cursor.fetchall():
|
|
927
|
+
if len(row) != 1 or not isinstance(row[0], str):
|
|
928
|
+
raise IntegrationInvalidResponseException("Oracle returned an invalid table name.")
|
|
929
|
+
names.append(row[0])
|
|
930
|
+
return names
|
|
931
|
+
|
|
932
|
+
async def _inner_ensure_collection_deleted(
|
|
933
|
+
self, collection_name: str, *, operation_options: Mapping[str, Any] | None = None
|
|
934
|
+
) -> None:
|
|
935
|
+
_validate_operation_options(operation_options)
|
|
936
|
+
await _drop_table(self._client, collection_name)
|
|
937
|
+
|
|
938
|
+
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
|
939
|
+
"""Close the owned pool when exiting the store context."""
|
|
940
|
+
await self.close()
|
|
941
|
+
|
|
942
|
+
async def close(self) -> None:
|
|
943
|
+
"""Close the store, leaving injected clients untouched."""
|
|
944
|
+
await self._client.close()
|
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-framework-oracle
|
|
3
|
+
Version: 1.0.0a261008
|
|
4
|
+
Summary: Oracle Database native vector stores for Microsoft Agent Framework.
|
|
5
|
+
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: agent-framework-core>=1.19.0,<2
|
|
20
|
+
Requires-Dist: oracledb>=2.2.0,<4
|
|
21
|
+
Project-URL: homepage, https://aka.ms/agent-framework
|
|
22
|
+
Project-URL: issues, https://github.com/microsoft/agent-framework/issues
|
|
23
|
+
Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
|
|
24
|
+
|
|
25
|
+
# Agent Framework Oracle vector store
|
|
26
|
+
|
|
27
|
+
An alpha connector for storing and searching native `VECTOR` columns in Oracle
|
|
28
|
+
Database 23ai or newer. `OracleCollection` implements async batch CRUD and
|
|
29
|
+
vector search; `OracleStore` shares a client across collections; `OracleSettings`
|
|
30
|
+
resolves connection configuration through Agent Framework.
|
|
31
|
+
|
|
32
|
+
## Installation and setup
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install agent-framework-oracle --pre
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.10+, Oracle Database 23ai or newer with `COMPATIBLE` set to
|
|
39
|
+
**23.4.0 or higher**, `python-oracledb` 2.2.x or 3.x, and permission to
|
|
40
|
+
create/drop tables in the connected user's schema. The connector uses the
|
|
41
|
+
driver's async Thin-mode API; it does not initialize the Thick client or
|
|
42
|
+
provision a database.
|
|
43
|
+
It does not create or alter schemas, vector indexes, or existing tables.
|
|
44
|
+
|
|
45
|
+
Set `ORACLE_DSN` (for example `localhost:1521/FREEPDB1`), `ORACLE_USER`, and
|
|
46
|
+
`ORACLE_PASSWORD`, or pass `dsn`, `user`, and `password` to either constructor.
|
|
47
|
+
Credentials are resolved in order: **explicit argument > selected `.env` file
|
|
48
|
+
> process environment**. A `.env` file is read only when `env_file_path` is
|
|
49
|
+
specified; `env_file_encoding` overrides UTF-8. `password` accepts `str` or
|
|
50
|
+
Agent Framework `SecretString` and is unwrapped only when opening a connection.
|
|
51
|
+
Missing or empty credentials are errors.
|
|
52
|
+
|
|
53
|
+
Alternatively, pass a configured `oracledb.AsyncConnection` or
|
|
54
|
+
`oracledb.AsyncConnectionPool` as `client` for advanced authentication. A
|
|
55
|
+
supplied client cannot be combined with connection settings and remains
|
|
56
|
+
caller-owned. A connector-created pool opens on first use and closes on
|
|
57
|
+
`close()` or async context exit. A collection obtained from a store borrows its
|
|
58
|
+
client; closing that collection does not close the store. The store must remain
|
|
59
|
+
open while its collections are used.
|
|
60
|
+
|
|
61
|
+
Writes on pooled connections (both connector-owned and caller-supplied pools)
|
|
62
|
+
are committed on success and rolled back on failure. Writes to a supplied
|
|
63
|
+
`AsyncConnection` are left in the caller's transaction; **the caller must
|
|
64
|
+
commit or roll back**. Oracle DDL commits independently of the surrounding
|
|
65
|
+
transaction. Batch upserts on a borrowed connection may leave partial pending
|
|
66
|
+
work on failure, so roll it back before reuse.
|
|
67
|
+
|
|
68
|
+
`ensure_collection_exists()` leaves an existing table unchanged and does not
|
|
69
|
+
validate its schema; use a matching definition or create a new table. It never
|
|
70
|
+
migrates data or changes existing indexes.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from dataclasses import dataclass
|
|
77
|
+
from typing import Annotated
|
|
78
|
+
|
|
79
|
+
from agent_framework import Filter, VectorStoreField, vectorstoremodel
|
|
80
|
+
from agent_framework_oracle import OracleStore
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@vectorstoremodel(collection_name="articles")
|
|
84
|
+
@dataclass
|
|
85
|
+
class Article:
|
|
86
|
+
id: Annotated[str, VectorStoreField("key")]
|
|
87
|
+
text: Annotated[str, VectorStoreField("data")]
|
|
88
|
+
embedding: Annotated[list[float] | None, VectorStoreField("vector", dimensions=3)] = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def main() -> None:
|
|
92
|
+
async with OracleStore() as store:
|
|
93
|
+
collection = store.get_collection(Article)
|
|
94
|
+
await collection.ensure_collection_exists()
|
|
95
|
+
await collection.upsert([Article("one", "Oracle vector search", [1, 0, 0])], generate_vectors=False)
|
|
96
|
+
results = await collection.search(
|
|
97
|
+
vector=[1, 0, 0], filter=Filter("text", "contains_text", "Oracle"), top=3
|
|
98
|
+
)
|
|
99
|
+
async for result in results:
|
|
100
|
+
print(result["record"].text, result["score"])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Pass `generate_vectors=False` for precomputed vectors, or configure an
|
|
107
|
+
`embedding_generator` for local generation. `get()` excludes vector columns by
|
|
108
|
+
default; pass `include_vectors=True` to restore them. `get(keys)` preserves
|
|
109
|
+
requested key order and duplicates, omitting missing keys; filtered retrieval
|
|
110
|
+
supports `top`, `skip`, and `order_by`. Both `get()` and `search()` execute
|
|
111
|
+
portable filters and paging in Oracle. The
|
|
112
|
+
[runnable sample](https://github.com/microsoft/agent-framework/blob/main/python/packages/oracle/samples/oracle_vectors.py)
|
|
113
|
+
creates and deletes a unique test table.
|
|
114
|
+
|
|
115
|
+
## Capabilities and limits
|
|
116
|
+
|
|
117
|
+
- A collection is one table in the **current user's schema**. Keys are
|
|
118
|
+
application-provided strings (up to 512 UTF-8 bytes), signed 64-bit integers,
|
|
119
|
+
or UUIDs; automatically generated keys are not supported.
|
|
120
|
+
- Multiple nullable native vector columns are supported. Vector fields may
|
|
121
|
+
declare `float`/`float32` (Oracle `FLOAT32`), `float64` (`FLOAT64`), or `int8`
|
|
122
|
+
(`INT8`); omitted element types use `FLOAT32`. Dimensions must be 1–65535.
|
|
123
|
+
Supplied vectors must be dense finite numeric sequences with the declared
|
|
124
|
+
dimensions. Binary and sparse vectors are not supported.
|
|
125
|
+
- Scalar data fields support `str` (up to 4000 UTF-8 bytes), `int` (signed
|
|
126
|
+
64-bit), `float` (`BINARY_DOUBLE`), and `bool` (stored as `NUMBER(1,0)`).
|
|
127
|
+
Oracle converts empty strings to `NULL`, so this connector rejects empty
|
|
128
|
+
string **values** rather than changing their meaning. JSON fields, nested
|
|
129
|
+
paths, provider annotations, data/full-text/vector indexes, and schema
|
|
130
|
+
migration are not supported.
|
|
131
|
+
- Search uses Oracle `VECTOR_DISTANCE` with an explicit metric. The default
|
|
132
|
+
is `COSINE` **distance**; `cosine_similarity`, Euclidean distance, dot
|
|
133
|
+
product, negative dot product, squared Euclidean distance, and Manhattan
|
|
134
|
+
distance are also available. Scores are native
|
|
135
|
+
metric values, **not probabilities**. For distances a `score_threshold` is
|
|
136
|
+
a maximum; for similarities/dot product it is a minimum. The cutoff is
|
|
137
|
+
applied in SQL **before** paging. No ANN indexes are created or managed;
|
|
138
|
+
Oracle may use an existing index if the table already has one. There is no
|
|
139
|
+
exact/approximate toggle, server-side embedding generation, or
|
|
140
|
+
keyword-hybrid search. Index-selected approximate searches may return fewer
|
|
141
|
+
than the requested number of results.
|
|
142
|
+
- Portable filters support scalar equality (including `NULL` and boolean
|
|
143
|
+
distinctions), `in`/`not_in`, numeric ranges, `is_null`, `is_not_null`,
|
|
144
|
+
`exists`, literal text `contains_text`/`starts_with`/`ends_with`, and
|
|
145
|
+
AND/OR/NOT. Finite `Decimal` values are supported for exact numeric equality
|
|
146
|
+
and membership; malformed UUID filter values compare as unequal.
|
|
147
|
+
Collection membership and nested filters are rejected.
|
|
148
|
+
Ordered filters on integer fields reject non-integral float operands;
|
|
149
|
+
ordered `Decimal` operands are not supported.
|
|
150
|
+
String comparison follows the configured Oracle collation. Input values
|
|
151
|
+
are bound; identifiers are checked and quoted as individual names.
|
|
152
|
+
|
|
153
|
+
## Opt-in database tests
|
|
154
|
+
|
|
155
|
+
The unit tests require no Oracle server. To run the live integration test,
|
|
156
|
+
provide **all three** `ORACLE_TEST_DSN`, `ORACLE_TEST_USER`, and
|
|
157
|
+
`ORACLE_TEST_PASSWORD` for an explicitly designated disposable Oracle 23ai+
|
|
158
|
+
schema where the user can create and drop tables:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
cd python
|
|
162
|
+
uv run --package agent-framework-oracle pytest packages/oracle/tests \
|
|
163
|
+
-m integration
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Without those variables, the integration test skips; it never falls back to
|
|
167
|
+
ordinary application credentials. The fixture creates a unique table and
|
|
168
|
+
deletes only that table.
|
|
169
|
+
|
|
170
|
+
## Documentation
|
|
171
|
+
|
|
172
|
+
- [Oracle Database AI Vector Search requirements](https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/overview-ai-vector-search.html)
|
|
173
|
+
- [python-oracledb 2.2 vector types](https://python-oracledb.readthedocs.io/en/v2.2.0/user_guide/vector_data_type.html)
|
|
174
|
+
- [Microsoft Agent Framework](https://learn.microsoft.com/agent-framework/)
|
|
175
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
agent_framework_oracle/__init__.py,sha256=pYXvYkoxx73J2xTDG14zpFH3Duce_B04cxY3mI6UAQM,466
|
|
2
|
+
agent_framework_oracle/_vector_store.py,sha256=O9ymwtoYJJH-snzjxeV6VQCRajrC9q1PJy-_w692i5Y,45303
|
|
3
|
+
agent_framework_oracle/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
agent_framework_oracle-1.0.0a261008.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
|
|
5
|
+
agent_framework_oracle-1.0.0a261008.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
6
|
+
agent_framework_oracle-1.0.0a261008.dist-info/METADATA,sha256=1ahokXg1xfwqIp7f5L90sD298X3MhfLaTUvSAzAqEhs,8488
|
|
7
|
+
agent_framework_oracle-1.0.0a261008.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|