select-ai 1.0.0__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,184 @@
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 json
9
+ from abc import ABC
10
+ from dataclasses import dataclass
11
+ from typing import List, Mapping, Optional
12
+
13
+ import oracledb
14
+
15
+ from select_ai._abc import SelectAIDataClass
16
+
17
+ from .provider import Provider
18
+
19
+
20
+ @dataclass
21
+ class ProfileAttributes(SelectAIDataClass):
22
+ """
23
+ Use this class to define attributes to manage and configure the behavior of
24
+ an AI profile
25
+
26
+ :param bool comments: True to include column comments in the metadata used
27
+ for generating SQL queries from natural language prompts.
28
+ :param bool constraints: True to include referential integrity constraints
29
+ such as primary and foreign keys in the metadata sent to the LLM.
30
+ :param bool conversation: Indicates if conversation history is enabled for
31
+ a profile.
32
+ :param str credential_name: The name of the credential to access the AI
33
+ provider APIs.
34
+ :param bool enforce_object_list: Specifies whether to restrict the LLM
35
+ to generate SQL that uses only tables covered by the object list.
36
+ :param int max_tokens: Denotes the number of tokens to return per
37
+ generation. Default is 1024.
38
+ :param List[Mapping] object_list: Array of JSON objects specifying
39
+ the owner and object names that are eligible for natural language
40
+ translation to SQL.
41
+ :param str object_list_mode: Specifies whether to send metadata for the
42
+ most relevant tables or all tables to the LLM. Supported values are -
43
+ 'automated' and 'all'
44
+ :param select_ai.Provider provider: AI Provider
45
+ :param str stop_tokens: The generated text will be terminated at the
46
+ beginning of the earliest stop sequence. Sequence will be incorporated
47
+ into the text. The attribute value must be a valid array of string values
48
+ in JSON format
49
+ :param float temperature: Temperature is a non-negative float number used
50
+ to tune the degree of randomness. Lower temperatures mean less random
51
+ generations.
52
+ :param str vector_index_name: Name of the vector index
53
+
54
+ """
55
+
56
+ annotations: Optional[str] = None
57
+ case_sensitive_values: Optional[bool] = None
58
+ comments: Optional[bool] = None
59
+ constraints: Optional[str] = None
60
+ conversation: Optional[bool] = None
61
+ credential_name: Optional[str] = None
62
+ enable_sources: Optional[bool] = None
63
+ enable_source_offsets: Optional[bool] = None
64
+ enforce_object_list: Optional[bool] = None
65
+ max_tokens: Optional[int] = 1024
66
+ object_list: Optional[List[Mapping]] = None
67
+ object_list_mode: Optional[str] = None
68
+ provider: Optional[Provider] = None
69
+ seed: Optional[str] = None
70
+ stop_tokens: Optional[str] = None
71
+ streaming: Optional[str] = None
72
+ temperature: Optional[float] = None
73
+ vector_index_name: Optional[str] = None
74
+
75
+ def __post_init__(self):
76
+ super().__post_init__()
77
+ if self.provider and not isinstance(self.provider, Provider):
78
+ raise ValueError(
79
+ f"'provider' must be an object of " f"type select_ai.Provider"
80
+ )
81
+
82
+ def json(self, exclude_null=True):
83
+ attributes = {}
84
+ for k, v in self.dict(exclude_null=exclude_null).items():
85
+ if isinstance(v, Provider):
86
+ for provider_k, provider_v in v.dict(
87
+ exclude_null=exclude_null
88
+ ).items():
89
+ attributes[Provider.key_alias(provider_k)] = provider_v
90
+ else:
91
+ attributes[k] = v
92
+ return json.dumps(attributes)
93
+
94
+ @classmethod
95
+ def create(cls, **kwargs):
96
+ provider_attributes = {}
97
+ profile_attributes = {}
98
+ for k, v in kwargs.items():
99
+ if isinstance(v, oracledb.LOB):
100
+ v = v.read()
101
+ if k in Provider.keys():
102
+ provider_attributes[Provider.key_alias(k)] = v
103
+ else:
104
+ profile_attributes[k] = v
105
+ provider = Provider.create(**provider_attributes)
106
+ profile_attributes["provider"] = provider
107
+ return ProfileAttributes(**profile_attributes)
108
+
109
+ @classmethod
110
+ async def async_create(cls, **kwargs):
111
+ provider_attributes = {}
112
+ profile_attributes = {}
113
+ for k, v in kwargs.items():
114
+ if isinstance(v, oracledb.AsyncLOB):
115
+ v = await v.read()
116
+ if k in Provider.keys():
117
+ provider_attributes[Provider.key_alias(k)] = v
118
+ else:
119
+ profile_attributes[k] = v
120
+ provider = Provider.create(**provider_attributes)
121
+ profile_attributes["provider"] = provider
122
+ return ProfileAttributes(**profile_attributes)
123
+
124
+ def set_attribute(self, key, value):
125
+ if key in Provider.keys() and not isinstance(value, Provider):
126
+ setattr(self.provider, key, value)
127
+ else:
128
+ setattr(self, key, value)
129
+
130
+
131
+ class BaseProfile(ABC):
132
+ """
133
+ BaseProfile is an abstract base class representing a Profile
134
+ for Select AI's interactions with AI service providers (LLMs).
135
+ Use either select_ai.Profile or select_ai.AsyncProfile to
136
+ instantiate an AI profile object.
137
+
138
+ :param str profile_name : Name of the profile
139
+
140
+ :param select_ai.ProfileAttributes attributes:
141
+ Object specifying AI profile attributes
142
+
143
+ :param str description: Description of the profile
144
+
145
+ :param bool merge: Fetches the profile
146
+ from database, merges the non-null attributes and saves it back
147
+ in the database. Default value is False
148
+
149
+ :param bool replace: Replaces the profile and attributes
150
+ in the database. Default value is False
151
+
152
+ :param bool raise_error_if_exists: Raise ProfileExistsError
153
+ if profile exists in the database and replace = False and
154
+ merge = False. Default value is True
155
+
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ profile_name: Optional[str] = None,
161
+ attributes: Optional[ProfileAttributes] = None,
162
+ description: Optional[str] = None,
163
+ merge: Optional[bool] = False,
164
+ replace: Optional[bool] = False,
165
+ raise_error_if_exists: Optional[bool] = True,
166
+ ):
167
+ """Initialize a base profile"""
168
+ self.profile_name = profile_name
169
+ if attributes and not isinstance(attributes, ProfileAttributes):
170
+ raise TypeError(
171
+ "'attributes' must be an object of type "
172
+ "select_ai.ProfileAttributes"
173
+ )
174
+ self.attributes = attributes
175
+ self.description = description
176
+ self.merge = merge
177
+ self.replace = replace
178
+ self.raise_error_if_exists = raise_error_if_exists
179
+
180
+ def __repr__(self):
181
+ return (
182
+ f"{self.__class__.__name__}(profile_name={self.profile_name}, "
183
+ f"attributes={self.attributes}, description={self.description})"
184
+ )
@@ -0,0 +1,274 @@
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 datetime
9
+ import json
10
+ from dataclasses import dataclass
11
+ from typing import AsyncGenerator, Iterator, Optional
12
+
13
+ import oracledb
14
+
15
+ from select_ai._abc import SelectAIDataClass
16
+ from select_ai.db import async_cursor, cursor
17
+ from select_ai.errors import ConversationNotFoundError
18
+ from select_ai.sql import (
19
+ GET_USER_CONVERSATION_ATTRIBUTES,
20
+ LIST_USER_CONVERSATIONS,
21
+ )
22
+
23
+ __all__ = ["AsyncConversation", "Conversation", "ConversationAttributes"]
24
+
25
+
26
+ @dataclass
27
+ class ConversationAttributes(SelectAIDataClass):
28
+ """Conversation Attributes
29
+
30
+ :param str title: Conversation Title
31
+ :param str description: Description of the conversation topic
32
+ :param datetime.timedelta retention_days: The number of days the conversation
33
+ will be stored in the database from its creation date. If value is 0, the
34
+ conversation will not be removed unless it is manually deleted by
35
+ delete
36
+ :param int conversation_length: Number of prompts to store for this
37
+ conversation
38
+
39
+ """
40
+
41
+ title: Optional[str] = "New Conversation"
42
+ description: Optional[str] = None
43
+ retention_days: Optional[datetime.timedelta] = datetime.timedelta(days=7)
44
+ conversation_length: Optional[int] = 10
45
+
46
+ def json(self, exclude_null=True):
47
+ attributes = {}
48
+ for k, v in self.dict(exclude_null=exclude_null).items():
49
+ if isinstance(v, datetime.timedelta):
50
+ attributes[k] = v.days
51
+ else:
52
+ attributes[k] = v
53
+ return json.dumps(attributes)
54
+
55
+
56
+ class _BaseConversation:
57
+
58
+ def __init__(
59
+ self,
60
+ conversation_id: Optional[str] = None,
61
+ attributes: Optional[ConversationAttributes] = None,
62
+ ):
63
+ self.conversation_id = conversation_id
64
+ self.attributes = attributes
65
+
66
+ def __repr__(self):
67
+ return (
68
+ f"{self.__class__.__name__}(conversation_id={self.conversation_id}, "
69
+ f"attributes={self.attributes})"
70
+ )
71
+
72
+
73
+ class Conversation(_BaseConversation):
74
+ """Conversation class can be used to create, update and delete
75
+ conversations in the database
76
+
77
+ Typical usage is to combine this conversation object with an AI
78
+ Profile.chat_session() to have context-aware conversations with
79
+ the LLM provider
80
+
81
+ :param str conversation_id: Conversation ID
82
+ :param ConversationAttributes attributes: Conversation attributes
83
+
84
+ """
85
+
86
+ def create(self) -> str:
87
+ """Creates a new conversation and returns the conversation_id
88
+ to be used in context-aware conversations with LLMs
89
+
90
+ :return: conversation_id
91
+ """
92
+ with cursor() as cr:
93
+ self.conversation_id = cr.callfunc(
94
+ "DBMS_CLOUD_AI.CREATE_CONVERSATION",
95
+ oracledb.DB_TYPE_VARCHAR,
96
+ keyword_parameters={"attributes": self.attributes.json()},
97
+ )
98
+ return self.conversation_id
99
+
100
+ def delete(self, force: bool = False):
101
+ """Drops the conversation"""
102
+ with cursor() as cr:
103
+ cr.callproc(
104
+ "DBMS_CLOUD_AI.DROP_CONVERSATION",
105
+ keyword_parameters={
106
+ "conversation_id": self.conversation_id,
107
+ "force": force,
108
+ },
109
+ )
110
+
111
+ def set_attributes(self, attributes: ConversationAttributes):
112
+ """Updates the attributes of the conversation in the database"""
113
+ with cursor() as cr:
114
+ cr.callproc(
115
+ "DBMS_CLOUD_AI.UPDATE_CONVERSATION",
116
+ keyword_parameters={
117
+ "conversation_id": self.conversation_id,
118
+ "attributes": attributes.json(),
119
+ },
120
+ )
121
+
122
+ def get_attributes(self) -> ConversationAttributes:
123
+ """Get attributes of the conversation from the database"""
124
+ with cursor() as cr:
125
+ cr.execute(
126
+ GET_USER_CONVERSATION_ATTRIBUTES,
127
+ conversation_id=self.conversation_id,
128
+ )
129
+ attributes = cr.fetchone()
130
+ if attributes:
131
+ conversation_title = attributes[0]
132
+ if attributes[1]:
133
+ description = attributes[1].read() # Oracle.LOB
134
+ else:
135
+ description = None
136
+ retention_days = attributes[2]
137
+ return ConversationAttributes(
138
+ title=conversation_title,
139
+ description=description,
140
+ retention_days=retention_days,
141
+ )
142
+ else:
143
+ raise ConversationNotFoundError(
144
+ conversation_id=self.conversation_id
145
+ )
146
+
147
+ @classmethod
148
+ def list(cls) -> Iterator["Conversation"]:
149
+ """List all conversations
150
+
151
+ :return: Iterator[VectorIndex]
152
+ """
153
+ with cursor() as cr:
154
+ cr.execute(
155
+ LIST_USER_CONVERSATIONS,
156
+ )
157
+ for row in cr.fetchall():
158
+ conversation_id = row[0]
159
+ conversation_title = row[1]
160
+ if row[2]:
161
+ description = row[2].read() # Oracle.LOB
162
+ else:
163
+ description = None
164
+ retention_days = row[3]
165
+ attributes = ConversationAttributes(
166
+ title=conversation_title,
167
+ description=description,
168
+ retention_days=retention_days,
169
+ )
170
+ yield cls(
171
+ attributes=attributes, conversation_id=conversation_id
172
+ )
173
+
174
+
175
+ class AsyncConversation(_BaseConversation):
176
+ """AsyncConversation class can be used to create, update and delete
177
+ conversations in the database in an async manner
178
+
179
+ Typical usage is to combine this conversation object with an
180
+ AsyncProfile.chat_session() to have context-aware conversations
181
+
182
+ :param str conversation_id: Conversation ID
183
+ :param ConversationAttributes attributes: Conversation attributes
184
+
185
+ """
186
+
187
+ async def create(self) -> str:
188
+ """Creates a new conversation and returns the conversation_id
189
+ to be used in context-aware conversations with LLMs
190
+
191
+ :return: conversation_id
192
+ """
193
+ async with async_cursor() as cr:
194
+ self.conversation_id = await cr.callfunc(
195
+ "DBMS_CLOUD_AI.CREATE_CONVERSATION",
196
+ oracledb.DB_TYPE_VARCHAR,
197
+ keyword_parameters={"attributes": self.attributes.json()},
198
+ )
199
+ return self.conversation_id
200
+
201
+ async def delete(self, force: bool = False):
202
+ """Delete the conversation"""
203
+ async with async_cursor() as cr:
204
+ await cr.callproc(
205
+ "DBMS_CLOUD_AI.DROP_CONVERSATION",
206
+ keyword_parameters={
207
+ "conversation_id": self.conversation_id,
208
+ "force": force,
209
+ },
210
+ )
211
+
212
+ async def set_attributes(self, attributes: ConversationAttributes):
213
+ """Updates the attributes of the conversation"""
214
+ with cursor() as cr:
215
+ cr.callproc(
216
+ "DBMS_CLOUD_AI.UPDATE_CONVERSATION",
217
+ keyword_parameters={
218
+ "conversation_id": self.conversation_id,
219
+ "attributes": attributes.json(),
220
+ },
221
+ )
222
+
223
+ async def get_attributes(self) -> ConversationAttributes:
224
+ """Get attributes of the conversation from the database"""
225
+ async with async_cursor() as cr:
226
+ await cr.execute(
227
+ GET_USER_CONVERSATION_ATTRIBUTES,
228
+ conversation_id=self.conversation_id,
229
+ )
230
+ attributes = await cr.fetchone()
231
+ if attributes:
232
+ conversation_title = attributes[0]
233
+ if attributes[1]:
234
+ description = await attributes[1].read() # Oracle.AsyncLOB
235
+ else:
236
+ description = None
237
+ retention_days = attributes[2]
238
+ return ConversationAttributes(
239
+ title=conversation_title,
240
+ description=description,
241
+ retention_days=retention_days,
242
+ )
243
+ else:
244
+ raise ConversationNotFoundError(
245
+ conversation_id=self.conversation_id
246
+ )
247
+
248
+ @classmethod
249
+ async def list(cls) -> AsyncGenerator["AsyncConversation", None]:
250
+ """List all conversations
251
+
252
+ :return: Iterator[VectorIndex]
253
+ """
254
+ async with async_cursor() as cr:
255
+ await cr.execute(
256
+ LIST_USER_CONVERSATIONS,
257
+ )
258
+ rows = await cr.fetchall()
259
+ for row in rows:
260
+ conversation_id = row[0]
261
+ conversation_title = row[1]
262
+ if row[2]:
263
+ description = await row[2].read() # Oracle.AsyncLOB
264
+ else:
265
+ description = None
266
+ retention_days = row[3]
267
+ attributes = ConversationAttributes(
268
+ title=conversation_title,
269
+ description=description,
270
+ retention_days=retention_days,
271
+ )
272
+ yield cls(
273
+ attributes=attributes, conversation_id=conversation_id
274
+ )
@@ -0,0 +1,135 @@
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 typing import Mapping
9
+
10
+ import oracledb
11
+
12
+ from .db import async_cursor, cursor
13
+
14
+ __all__ = [
15
+ "async_create_credential",
16
+ "async_delete_credential",
17
+ "create_credential",
18
+ "delete_credential",
19
+ ]
20
+
21
+
22
+ def _validate_credential(credential: Mapping[str, str]):
23
+ valid_keys = {
24
+ "credential_name",
25
+ "username",
26
+ "password",
27
+ "user_ocid",
28
+ "tenancy_ocid",
29
+ "private_key",
30
+ "fingerprint",
31
+ "comments",
32
+ }
33
+ for k in credential.keys():
34
+ if k.lower() not in valid_keys:
35
+ raise ValueError(
36
+ f"Invalid value {k}: {credential[k]} for credential object"
37
+ )
38
+
39
+
40
+ async def async_create_credential(credential: Mapping, replace: bool = False):
41
+ """
42
+ Async API to create credential.
43
+
44
+ Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL. if replace
45
+ is True, credential will be replaced if it already exists
46
+
47
+ """
48
+ _validate_credential(credential)
49
+ async with async_cursor() as cr:
50
+ try:
51
+ await cr.callproc(
52
+ "DBMS_CLOUD.CREATE_CREDENTIAL", keyword_parameters=credential
53
+ )
54
+ except oracledb.DatabaseError as e:
55
+ (error,) = e.args
56
+ # If already exists and replace is True then drop and recreate
57
+ if error.code == 20022 and replace:
58
+ await cr.callproc(
59
+ "DBMS_CLOUD.DROP_CREDENTIAL",
60
+ keyword_parameters={
61
+ "credential_name": credential["credential_name"]
62
+ },
63
+ )
64
+ await cr.callproc(
65
+ "DBMS_CLOUD.CREATE_CREDENTIAL",
66
+ keyword_parameters=credential,
67
+ )
68
+ else:
69
+ raise
70
+
71
+
72
+ async def async_delete_credential(credential_name: str, force: bool = False):
73
+ """
74
+ Async API to create credential.
75
+
76
+ Deletes a credential object using DBMS_CLOUD.DROP_CREDENTIAL
77
+ """
78
+ async with async_cursor() as cr:
79
+ try:
80
+ await cr.callproc(
81
+ "DBMS_CLOUD.DROP_CREDENTIAL",
82
+ keyword_parameters={"credential_name": credential_name},
83
+ )
84
+ except oracledb.DatabaseError as e:
85
+ (error,) = e.args
86
+ if error.code == 20004 and force: # does not exist
87
+ pass
88
+ else:
89
+ raise
90
+
91
+
92
+ def create_credential(credential: Mapping, replace: bool = False):
93
+ """
94
+
95
+ Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL. if replace
96
+ is True, credential will be replaced if it "already exists"
97
+
98
+ """
99
+ _validate_credential(credential)
100
+ with cursor() as cr:
101
+ try:
102
+ cr.callproc(
103
+ "DBMS_CLOUD.CREATE_CREDENTIAL", keyword_parameters=credential
104
+ )
105
+ except oracledb.DatabaseError as e:
106
+ (error,) = e.args
107
+ # If already exists and replace is True then drop and recreate
108
+ if error.code == 20022 and replace:
109
+ cr.callproc(
110
+ "DBMS_CLOUD.DROP_CREDENTIAL",
111
+ keyword_parameters={
112
+ "credential_name": credential["credential_name"]
113
+ },
114
+ )
115
+ cr.callproc(
116
+ "DBMS_CLOUD.CREATE_CREDENTIAL",
117
+ keyword_parameters=credential,
118
+ )
119
+ else:
120
+ raise
121
+
122
+
123
+ def delete_credential(credential_name: str, force: bool = False):
124
+ with cursor() as cr:
125
+ try:
126
+ cr.callproc(
127
+ "DBMS_CLOUD.DROP_CREDENTIAL",
128
+ keyword_parameters={"credential_name": credential_name},
129
+ )
130
+ except oracledb.DatabaseError as e:
131
+ (error,) = e.args
132
+ if error.code == 20004 and force: # does not exist
133
+ pass
134
+ else:
135
+ raise