select-ai 1.0.0.dev4__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 select-ai might be problematic. Click here for more details.
- select_ai/__init__.py +52 -0
- select_ai/_abc.py +74 -0
- select_ai/_enums.py +14 -0
- select_ai/action.py +21 -0
- select_ai/admin.py +108 -0
- select_ai/async_profile.py +468 -0
- select_ai/base_profile.py +166 -0
- select_ai/conversation.py +249 -0
- select_ai/db.py +171 -0
- select_ai/errors.py +49 -0
- select_ai/profile.py +397 -0
- select_ai/provider.py +187 -0
- select_ai/sql.py +105 -0
- select_ai/synthetic_data.py +84 -0
- select_ai/vector_index.py +542 -0
- select_ai/version.py +8 -0
- select_ai-1.0.0.dev4.dist-info/METADATA +25 -0
- select_ai-1.0.0.dev4.dist-info/RECORD +21 -0
- select_ai-1.0.0.dev4.dist-info/WHEEL +5 -0
- select_ai-1.0.0.dev4.dist-info/licenses/LICENSE.txt +35 -0
- select_ai-1.0.0.dev4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2025, Oracle and/or its affiliates.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at
|
|
5
|
+
# http://oss.oracle.com/licenses/upl.
|
|
6
|
+
# -----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import AsyncGenerator, Iterator, Optional
|
|
10
|
+
|
|
11
|
+
import oracledb
|
|
12
|
+
|
|
13
|
+
from select_ai._abc import SelectAIDataClass
|
|
14
|
+
from select_ai.db import async_cursor, cursor
|
|
15
|
+
from select_ai.errors import ConversationNotFoundError
|
|
16
|
+
from select_ai.sql import (
|
|
17
|
+
GET_USER_CONVERSATION_ATTRIBUTES,
|
|
18
|
+
LIST_USER_CONVERSATIONS,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = ["AsyncConversation", "Conversation", "ConversationAttributes"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ConversationAttributes(SelectAIDataClass):
|
|
26
|
+
"""Conversation Attributes
|
|
27
|
+
|
|
28
|
+
:param str title: Conversation Title
|
|
29
|
+
:param str description: Description of the conversation topic
|
|
30
|
+
:param int retention_days: The number of days the conversation will be
|
|
31
|
+
stored in the database from its creation date. If value is 0, we will
|
|
32
|
+
not remove the conversation unless it is manually deleted by
|
|
33
|
+
drop_conversation
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
title: Optional[str] = "New Conversation"
|
|
38
|
+
description: Optional[str] = None
|
|
39
|
+
retention_days: Optional[int] = 7
|
|
40
|
+
# conversation_length: Optional[int] = 10
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _BaseConversation:
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
conversation_id: Optional[str] = None,
|
|
48
|
+
attributes: Optional[ConversationAttributes] = None,
|
|
49
|
+
):
|
|
50
|
+
self.conversation_id = conversation_id
|
|
51
|
+
self.attributes = attributes
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
return (
|
|
55
|
+
f"{self.__class__.__name__}(conversation_id={self.conversation_id}, "
|
|
56
|
+
f"attributes={self.attributes})"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Conversation(_BaseConversation):
|
|
61
|
+
"""Conversation class can be used to create, update and delete
|
|
62
|
+
conversations in the database
|
|
63
|
+
|
|
64
|
+
Typical usage is to combine this conversation object with an AI
|
|
65
|
+
Profile.chat_session() to have context-aware conversations with
|
|
66
|
+
the LLM provider
|
|
67
|
+
|
|
68
|
+
:param str conversation_id: Conversation ID
|
|
69
|
+
:param ConversationAttributes attributes: Conversation attributes
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def create(self) -> str:
|
|
74
|
+
"""Creates a new conversation and returns the conversation_id
|
|
75
|
+
to be used in context-aware conversations with LLMs
|
|
76
|
+
|
|
77
|
+
:return: conversation_id
|
|
78
|
+
"""
|
|
79
|
+
with cursor() as cr:
|
|
80
|
+
self.conversation_id = cr.callfunc(
|
|
81
|
+
"DBMS_CLOUD_AI.CREATE_CONVERSATION",
|
|
82
|
+
oracledb.DB_TYPE_VARCHAR,
|
|
83
|
+
keyword_parameters={"attributes": self.attributes.json()},
|
|
84
|
+
)
|
|
85
|
+
return self.conversation_id
|
|
86
|
+
|
|
87
|
+
def delete(self, force: bool = False):
|
|
88
|
+
"""Drops the conversation"""
|
|
89
|
+
with cursor() as cr:
|
|
90
|
+
cr.callproc(
|
|
91
|
+
"DBMS_CLOUD_AI.DROP_CONVERSATION",
|
|
92
|
+
keyword_parameters={
|
|
93
|
+
"conversation_id": self.conversation_id,
|
|
94
|
+
"force": force,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def set_attributes(self, attributes: ConversationAttributes):
|
|
99
|
+
"""Updates the attributes of the conversation in the database"""
|
|
100
|
+
with cursor() as cr:
|
|
101
|
+
cr.callproc(
|
|
102
|
+
"DBMS_CLOUD_AI.UPDATE_CONVERSATION",
|
|
103
|
+
keyword_parameters={
|
|
104
|
+
"conversation_id": self.conversation_id,
|
|
105
|
+
"attributes": attributes.json(),
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def get_attributes(self) -> ConversationAttributes:
|
|
110
|
+
"""Get attributes of the conversation from the database"""
|
|
111
|
+
with cursor() as cr:
|
|
112
|
+
cr.execute(
|
|
113
|
+
GET_USER_CONVERSATION_ATTRIBUTES,
|
|
114
|
+
conversation_id=self.conversation_id,
|
|
115
|
+
)
|
|
116
|
+
attributes = cr.fetchone()
|
|
117
|
+
if attributes:
|
|
118
|
+
conversation_title = attributes[0]
|
|
119
|
+
description = attributes[1].read() # Oracle.LOB
|
|
120
|
+
retention_days = attributes[2]
|
|
121
|
+
return ConversationAttributes(
|
|
122
|
+
title=conversation_title,
|
|
123
|
+
description=description,
|
|
124
|
+
retention_days=retention_days,
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
raise ConversationNotFoundError(
|
|
128
|
+
conversation_id=self.conversation_id
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def list(cls) -> Iterator["Conversation"]:
|
|
133
|
+
"""List all conversations
|
|
134
|
+
|
|
135
|
+
:return: Iterator[VectorIndex]
|
|
136
|
+
"""
|
|
137
|
+
with cursor() as cr:
|
|
138
|
+
cr.execute(
|
|
139
|
+
LIST_USER_CONVERSATIONS,
|
|
140
|
+
)
|
|
141
|
+
for row in cr.fetchall():
|
|
142
|
+
conversation_id = row[0]
|
|
143
|
+
conversation_title = row[1]
|
|
144
|
+
description = row[2].read() # Oracle.LOB
|
|
145
|
+
retention_days = row[3]
|
|
146
|
+
attributes = ConversationAttributes(
|
|
147
|
+
title=conversation_title,
|
|
148
|
+
description=description,
|
|
149
|
+
retention_days=retention_days,
|
|
150
|
+
)
|
|
151
|
+
yield cls(
|
|
152
|
+
attributes=attributes, conversation_id=conversation_id
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class AsyncConversation(_BaseConversation):
|
|
157
|
+
"""AsyncConversation class can be used to create, update and delete
|
|
158
|
+
conversations in the database in an async manner
|
|
159
|
+
|
|
160
|
+
Typical usage is to combine this conversation object with an
|
|
161
|
+
AsyncProfile.chat_session() to have context-aware conversations
|
|
162
|
+
|
|
163
|
+
:param str conversation_id: Conversation ID
|
|
164
|
+
:param ConversationAttributes attributes: Conversation attributes
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
async def create(self) -> str:
|
|
169
|
+
"""Creates a new conversation and returns the conversation_id
|
|
170
|
+
to be used in context-aware conversations with LLMs
|
|
171
|
+
|
|
172
|
+
:return: conversation_id
|
|
173
|
+
"""
|
|
174
|
+
async with async_cursor() as cr:
|
|
175
|
+
self.conversation_id = await cr.callfunc(
|
|
176
|
+
"DBMS_CLOUD_AI.CREATE_CONVERSATION",
|
|
177
|
+
oracledb.DB_TYPE_VARCHAR,
|
|
178
|
+
keyword_parameters={"attributes": self.attributes.json()},
|
|
179
|
+
)
|
|
180
|
+
return self.conversation_id
|
|
181
|
+
|
|
182
|
+
async def delete(self, force: bool = False):
|
|
183
|
+
"""Delete the conversation"""
|
|
184
|
+
async with async_cursor() as cr:
|
|
185
|
+
await cr.callproc(
|
|
186
|
+
"DBMS_CLOUD_AI.DROP_CONVERSATION",
|
|
187
|
+
keyword_parameters={
|
|
188
|
+
"conversation_id": self.conversation_id,
|
|
189
|
+
"force": force,
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def set_attributes(self, attributes: ConversationAttributes):
|
|
194
|
+
"""Updates the attributes of the conversation"""
|
|
195
|
+
with cursor() as cr:
|
|
196
|
+
cr.callproc(
|
|
197
|
+
"DBMS_CLOUD_AI.UPDATE_CONVERSATION",
|
|
198
|
+
keyword_parameters={
|
|
199
|
+
"conversation_id": self.conversation_id,
|
|
200
|
+
"attributes": attributes.json(),
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def get_attributes(self) -> ConversationAttributes:
|
|
205
|
+
"""Get attributes of the conversation from the database"""
|
|
206
|
+
async with async_cursor() as cr:
|
|
207
|
+
await cr.execute(
|
|
208
|
+
GET_USER_CONVERSATION_ATTRIBUTES,
|
|
209
|
+
conversation_id=self.conversation_id,
|
|
210
|
+
)
|
|
211
|
+
attributes = await cr.fetchone()
|
|
212
|
+
if attributes:
|
|
213
|
+
conversation_title = attributes[0]
|
|
214
|
+
description = await attributes[1].read() # Oracle.AsyncLOB
|
|
215
|
+
retention_days = attributes[2]
|
|
216
|
+
return ConversationAttributes(
|
|
217
|
+
title=conversation_title,
|
|
218
|
+
description=description,
|
|
219
|
+
retention_days=retention_days,
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
raise ConversationNotFoundError(
|
|
223
|
+
conversation_id=self.conversation_id
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
async def list(cls) -> AsyncGenerator["AsyncConversation", None]:
|
|
228
|
+
"""List all conversations
|
|
229
|
+
|
|
230
|
+
:return: Iterator[VectorIndex]
|
|
231
|
+
"""
|
|
232
|
+
async with async_cursor() as cr:
|
|
233
|
+
await cr.execute(
|
|
234
|
+
LIST_USER_CONVERSATIONS,
|
|
235
|
+
)
|
|
236
|
+
rows = await cr.fetchall()
|
|
237
|
+
for row in rows:
|
|
238
|
+
conversation_id = row[0]
|
|
239
|
+
conversation_title = row[1]
|
|
240
|
+
description = await row[2].read() # Oracle.AsyncLOB
|
|
241
|
+
retention_days = row[3]
|
|
242
|
+
attributes = ConversationAttributes(
|
|
243
|
+
title=conversation_title,
|
|
244
|
+
description=description,
|
|
245
|
+
retention_days=retention_days,
|
|
246
|
+
)
|
|
247
|
+
yield cls(
|
|
248
|
+
attributes=attributes, conversation_id=conversation_id
|
|
249
|
+
)
|
select_ai/db.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2025, Oracle and/or its affiliates.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at
|
|
5
|
+
# http://oss.oracle.com/licenses/upl.
|
|
6
|
+
# -----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
import contextlib
|
|
9
|
+
import os
|
|
10
|
+
from threading import get_ident
|
|
11
|
+
from typing import Dict, Hashable
|
|
12
|
+
|
|
13
|
+
import oracledb
|
|
14
|
+
|
|
15
|
+
__conn__: Dict[Hashable, oracledb.Connection] = {}
|
|
16
|
+
__async_conn__: Dict[Hashable, oracledb.AsyncConnection] = {}
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"connect",
|
|
20
|
+
"async_connect",
|
|
21
|
+
"is_connected",
|
|
22
|
+
"async_is_connected",
|
|
23
|
+
"get_connection",
|
|
24
|
+
"async_get_connection",
|
|
25
|
+
"cursor",
|
|
26
|
+
"async_cursor",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def connect(user: str, password: str, dsn: str, *args, **kwargs):
|
|
31
|
+
"""Creates an oracledb.Connection object
|
|
32
|
+
and saves it global dictionary __conn__
|
|
33
|
+
The connection object is thread local meaning
|
|
34
|
+
in a multithreaded application, individual
|
|
35
|
+
threads cannot see each other's connection
|
|
36
|
+
object
|
|
37
|
+
"""
|
|
38
|
+
conn = oracledb.connect(
|
|
39
|
+
user=user,
|
|
40
|
+
password=password,
|
|
41
|
+
dsn=dsn,
|
|
42
|
+
connection_id_prefix="python-select-ai",
|
|
43
|
+
*args,
|
|
44
|
+
**kwargs,
|
|
45
|
+
)
|
|
46
|
+
_set_connection(conn=conn)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def async_connect(user: str, password: str, dsn: str, *args, **kwargs):
|
|
50
|
+
"""Creates an oracledb.AsyncConnection object
|
|
51
|
+
and saves it global dictionary __async_conn__
|
|
52
|
+
The connection object is thread local meaning
|
|
53
|
+
in a multithreaded application, individual
|
|
54
|
+
threads cannot see each other's connection
|
|
55
|
+
object
|
|
56
|
+
"""
|
|
57
|
+
async_conn = await oracledb.connect_async(
|
|
58
|
+
user=user, password=password, dsn=dsn, *args, **kwargs
|
|
59
|
+
)
|
|
60
|
+
_set_connection(async_conn=async_conn)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_connected() -> bool:
|
|
64
|
+
"""Checks if database connection is open and healthy"""
|
|
65
|
+
global __conn__
|
|
66
|
+
key = (os.getpid(), get_ident())
|
|
67
|
+
conn = __conn__.get(key)
|
|
68
|
+
if conn is None:
|
|
69
|
+
return False
|
|
70
|
+
try:
|
|
71
|
+
return conn.ping() is None
|
|
72
|
+
except oracledb.DatabaseError:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def async_is_connected() -> bool:
|
|
77
|
+
"""Asynchronously checks if database connection is open and healthy"""
|
|
78
|
+
|
|
79
|
+
global __async_conn__
|
|
80
|
+
key = (os.getpid(), get_ident())
|
|
81
|
+
conn = __async_conn__.get(key)
|
|
82
|
+
if conn is None:
|
|
83
|
+
return False
|
|
84
|
+
try:
|
|
85
|
+
return await conn.ping() is None
|
|
86
|
+
except oracledb.DatabaseError:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _set_connection(
|
|
91
|
+
conn: oracledb.Connection = None,
|
|
92
|
+
async_conn: oracledb.AsyncConnection = None,
|
|
93
|
+
):
|
|
94
|
+
"""Set existing connection for select_ai Python API to reuse
|
|
95
|
+
|
|
96
|
+
:param conn: python-oracledb Connection object
|
|
97
|
+
:param async_conn: python-oracledb
|
|
98
|
+
:return:
|
|
99
|
+
"""
|
|
100
|
+
key = (os.getpid(), get_ident())
|
|
101
|
+
if conn:
|
|
102
|
+
global __conn__
|
|
103
|
+
__conn__[key] = conn
|
|
104
|
+
if async_conn:
|
|
105
|
+
global __async_conn__
|
|
106
|
+
__async_conn__[key] = async_conn
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_connection() -> oracledb.Connection:
|
|
110
|
+
"""Returns the connection object if connection is healthy"""
|
|
111
|
+
if not is_connected():
|
|
112
|
+
raise Exception(
|
|
113
|
+
"Not connected to the Database. "
|
|
114
|
+
"Use select_ai.db.connect() to establish connection"
|
|
115
|
+
)
|
|
116
|
+
global __conn__
|
|
117
|
+
key = (os.getpid(), get_ident())
|
|
118
|
+
return __conn__[key]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def async_get_connection() -> oracledb.AsyncConnection:
|
|
122
|
+
"""Returns the AsyncConnection object if connection is healthy"""
|
|
123
|
+
if not await async_is_connected():
|
|
124
|
+
raise Exception(
|
|
125
|
+
"Not connected to the Database. "
|
|
126
|
+
"Use select_ai.db.async_connect() to establish "
|
|
127
|
+
"connection"
|
|
128
|
+
)
|
|
129
|
+
global __async_conn__
|
|
130
|
+
key = (os.getpid(), get_ident())
|
|
131
|
+
return __async_conn__[key]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@contextlib.contextmanager
|
|
135
|
+
def cursor():
|
|
136
|
+
"""
|
|
137
|
+
Creates a context manager for database cursor
|
|
138
|
+
|
|
139
|
+
Typical usage:
|
|
140
|
+
|
|
141
|
+
with select_ai.cursor() as cr:
|
|
142
|
+
cr.execute(<QUERY>)
|
|
143
|
+
|
|
144
|
+
This ensures that the cursor is closed regardless
|
|
145
|
+
of whether an exception occurred
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
cr = get_connection().cursor()
|
|
149
|
+
try:
|
|
150
|
+
yield cr
|
|
151
|
+
finally:
|
|
152
|
+
cr.close()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@contextlib.asynccontextmanager
|
|
156
|
+
async def async_cursor():
|
|
157
|
+
"""
|
|
158
|
+
Creates an async context manager for database cursor
|
|
159
|
+
|
|
160
|
+
Typical usage:
|
|
161
|
+
|
|
162
|
+
async with select_ai.cursor() as cr:
|
|
163
|
+
await cr.execute(<QUERY>)
|
|
164
|
+
:return:
|
|
165
|
+
"""
|
|
166
|
+
conn = await async_get_connection()
|
|
167
|
+
cr = conn.cursor()
|
|
168
|
+
try:
|
|
169
|
+
yield cr
|
|
170
|
+
finally:
|
|
171
|
+
cr.close()
|
select_ai/errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2025, Oracle and/or its affiliates.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at
|
|
5
|
+
# http://oss.oracle.com/licenses/upl.
|
|
6
|
+
# -----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SelectAIError(Exception):
|
|
10
|
+
"""Base class for any SelectAIErrors"""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConversationNotFoundError(SelectAIError):
|
|
16
|
+
"""Conversation not found in the database"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, conversation_id: str):
|
|
19
|
+
self.conversation_id = conversation_id
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return f"Conversation with id {self.conversation_id} not found"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProfileNotFoundError(SelectAIError):
|
|
26
|
+
"""Profile not found in the database"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, profile_name: str):
|
|
29
|
+
self.profile_name = profile_name
|
|
30
|
+
|
|
31
|
+
def __str__(self):
|
|
32
|
+
return f"Profile {self.profile_name} not found"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VectorIndexNotFoundError(SelectAIError):
|
|
36
|
+
"""VectorIndex not found in the database"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, index_name: str, profile_name: str = None):
|
|
39
|
+
self.index_name = index_name
|
|
40
|
+
self.profile_name = profile_name
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
if self.profile_name:
|
|
44
|
+
return (
|
|
45
|
+
f"VectorIndex {self.index_name} "
|
|
46
|
+
f"not found for profile {self.profile_name}"
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
return f"VectorIndex {self.index_name} not found"
|