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.

@@ -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"