select-ai 1.0.0.dev6__py3-none-any.whl → 1.0.0.dev8__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 CHANGED
@@ -6,11 +6,6 @@
6
6
  # -----------------------------------------------------------------------------
7
7
 
8
8
  from .action import Action
9
- from .admin import (
10
- create_credential,
11
- disable_provider,
12
- enable_provider,
13
- )
14
9
  from .async_profile import AsyncProfile
15
10
  from .base_profile import BaseProfile, ProfileAttributes
16
11
  from .conversation import (
@@ -18,6 +13,12 @@ from .conversation import (
18
13
  Conversation,
19
14
  ConversationAttributes,
20
15
  )
16
+ from .credential import (
17
+ async_create_credential,
18
+ async_delete_credential,
19
+ create_credential,
20
+ delete_credential,
21
+ )
21
22
  from .db import (
22
23
  async_connect,
23
24
  async_cursor,
@@ -39,6 +40,10 @@ from .provider import (
39
40
  OCIGenAIProvider,
40
41
  OpenAIProvider,
41
42
  Provider,
43
+ async_disable_provider,
44
+ async_enable_provider,
45
+ disable_provider,
46
+ enable_provider,
42
47
  )
43
48
  from .synthetic_data import (
44
49
  SyntheticDataAttributes,
@@ -55,7 +55,7 @@ class AsyncProfile(BaseProfile):
55
55
  :return: None
56
56
  :raises: oracledb.DatabaseError
57
57
  """
58
- if self.profile_name is not None:
58
+ if self.profile_name:
59
59
  profile_exists = False
60
60
  try:
61
61
  saved_attributes = await self._get_attributes(
@@ -75,7 +75,7 @@ class AsyncProfile(BaseProfile):
75
75
  profile_name=self.profile_name
76
76
  )
77
77
  except ProfileNotFoundError:
78
- if self.attributes is None:
78
+ if self.attributes is None and self.description is None:
79
79
  raise
80
80
  else:
81
81
  if self.attributes is None:
@@ -91,10 +91,13 @@ class AsyncProfile(BaseProfile):
91
91
  await self.create(
92
92
  replace=self.replace, description=self.description
93
93
  )
94
+ else: # profile name is None:
95
+ if self.attributes is not None or self.description is not None:
96
+ raise ValueError("'profile_name' cannot be empty or None")
94
97
  return self
95
98
 
96
99
  @staticmethod
97
- async def _get_profile_description(profile_name) -> str:
100
+ async def _get_profile_description(profile_name) -> Union[str, None]:
98
101
  """Get description of profile from USER_CLOUD_AI_PROFILES
99
102
 
100
103
  :param str profile_name: Name of profile
@@ -110,7 +113,10 @@ class AsyncProfile(BaseProfile):
110
113
  )
111
114
  profile = await cr.fetchone()
112
115
  if profile:
113
- return await profile[1].read()
116
+ if profile[1] is not None:
117
+ return await profile[1].read()
118
+ else:
119
+ return None
114
120
  else:
115
121
  raise ProfileNotFoundError(profile_name)
116
122
 
@@ -186,6 +192,12 @@ class AsyncProfile(BaseProfile):
186
192
  attributes
187
193
  :return: None
188
194
  """
195
+ if not isinstance(attributes, ProfileAttributes):
196
+ raise TypeError(
197
+ "'attributes' must be an object of type "
198
+ "select_ai.ProfileAttributes"
199
+ )
200
+
189
201
  self.attributes = attributes
190
202
  parameters = {
191
203
  "profile_name": self.profile_name,
@@ -206,13 +218,14 @@ class AsyncProfile(BaseProfile):
206
218
  :return: None
207
219
  :raises: oracledb.DatabaseError
208
220
  """
221
+ if self.attributes is None:
222
+ raise AttributeError("Profile attributes cannot be None")
209
223
  parameters = {
210
224
  "profile_name": self.profile_name,
211
225
  "attributes": self.attributes.json(),
212
226
  }
213
227
  if description:
214
228
  parameters["description"] = description
215
-
216
229
  async with async_cursor() as cr:
217
230
  try:
218
231
  await cr.callproc(
@@ -222,7 +235,7 @@ class AsyncProfile(BaseProfile):
222
235
  except oracledb.DatabaseError as e:
223
236
  (error,) = e.args
224
237
  # If already exists and replace is True then drop and recreate
225
- if "already exists" in error.message.lower() and replace:
238
+ if error.code == 20046 and replace:
226
239
  await self.delete(force=True)
227
240
  await cr.callproc(
228
241
  "DBMS_CLOUD_AI.CREATE_PROFILE",
@@ -272,13 +285,13 @@ class AsyncProfile(BaseProfile):
272
285
 
273
286
  @classmethod
274
287
  async def list(
275
- cls, profile_name_pattern: str
288
+ cls, profile_name_pattern: str = ".*"
276
289
  ) -> AsyncGenerator["AsyncProfile", None]:
277
290
  """Asynchronously list AI Profiles saved in the database.
278
291
 
279
292
  :param str profile_name_pattern: Regular expressions can be used
280
293
  to specify a pattern. Function REGEXP_LIKE is used to perform the
281
- match
294
+ match. Default value is ".*" i.e. match all AI profiles.
282
295
 
283
296
  :return: Iterator[Profile]
284
297
  """
@@ -302,7 +315,7 @@ class AsyncProfile(BaseProfile):
302
315
  )
303
316
 
304
317
  async def generate(
305
- self, prompt, action=Action.SHOWSQL, params: Mapping = None
318
+ self, prompt: str, action=Action.SHOWSQL, params: Mapping = None
306
319
  ) -> Union[pandas.DataFrame, str, None]:
307
320
  """Asynchronously perform AI translation using this profile
308
321
 
@@ -312,6 +325,9 @@ class AsyncProfile(BaseProfile):
312
325
  conversation_id for context-aware chats
313
326
  :return: Union[pandas.DataFrame, str]
314
327
  """
328
+ if not prompt:
329
+ raise ValueError("prompt cannot be empty or None")
330
+
315
331
  parameters = {
316
332
  "prompt": prompt,
317
333
  "action": action,
@@ -431,6 +447,13 @@ class AsyncProfile(BaseProfile):
431
447
  :raises: oracledb.DatabaseError
432
448
 
433
449
  """
450
+ if synthetic_data_attributes is None:
451
+ raise ValueError("'synthetic_data_attributes' cannot be None")
452
+
453
+ if not isinstance(synthetic_data_attributes, SyntheticDataAttributes):
454
+ raise TypeError("'synthetic_data_attributes' must be an object "
455
+ "of type select_ai.SyntheticDataAttributes")
456
+
434
457
  keyword_parameters = synthetic_data_attributes.prepare()
435
458
  keyword_parameters["profile_name"] = self.profile_name
436
459
  async with async_cursor() as cr:
select_ai/base_profile.py CHANGED
@@ -73,10 +73,9 @@ class ProfileAttributes(SelectAIDataClass):
73
73
  vector_index_name: Optional[str] = None
74
74
 
75
75
  def __post_init__(self):
76
- if not isinstance(self.provider, Provider):
76
+ if self.provider and not isinstance(self.provider, Provider):
77
77
  raise ValueError(
78
- f"The arg `provider` must be an object of "
79
- f"type select_ai.Provider"
78
+ f"'provider' must be an object of " f"type select_ai.Provider"
80
79
  )
81
80
 
82
81
  def json(self, exclude_null=True):
@@ -166,6 +165,11 @@ class BaseProfile(ABC):
166
165
  ):
167
166
  """Initialize a base profile"""
168
167
  self.profile_name = profile_name
168
+ if attributes and not isinstance(attributes, ProfileAttributes):
169
+ raise TypeError(
170
+ "'attributes' must be an object of type "
171
+ "select_ai.ProfileAttributes"
172
+ )
169
173
  self.attributes = attributes
170
174
  self.description = description
171
175
  self.merge = merge
select_ai/conversation.py CHANGED
@@ -129,7 +129,10 @@ class Conversation(_BaseConversation):
129
129
  attributes = cr.fetchone()
130
130
  if attributes:
131
131
  conversation_title = attributes[0]
132
- description = attributes[1].read() # Oracle.LOB
132
+ if attributes[1]:
133
+ description = attributes[1].read() # Oracle.LOB
134
+ else:
135
+ description = None
133
136
  retention_days = attributes[2]
134
137
  return ConversationAttributes(
135
138
  title=conversation_title,
@@ -154,7 +157,10 @@ class Conversation(_BaseConversation):
154
157
  for row in cr.fetchall():
155
158
  conversation_id = row[0]
156
159
  conversation_title = row[1]
157
- description = row[2].read() # Oracle.LOB
160
+ if row[2]:
161
+ description = row[2].read() # Oracle.LOB
162
+ else:
163
+ description = None
158
164
  retention_days = row[3]
159
165
  attributes = ConversationAttributes(
160
166
  title=conversation_title,
@@ -224,7 +230,10 @@ class AsyncConversation(_BaseConversation):
224
230
  attributes = await cr.fetchone()
225
231
  if attributes:
226
232
  conversation_title = attributes[0]
227
- description = await attributes[1].read() # Oracle.AsyncLOB
233
+ if attributes[1]:
234
+ description = await attributes[1].read() # Oracle.AsyncLOB
235
+ else:
236
+ description = None
228
237
  retention_days = attributes[2]
229
238
  return ConversationAttributes(
230
239
  title=conversation_title,
@@ -250,7 +259,10 @@ class AsyncConversation(_BaseConversation):
250
259
  for row in rows:
251
260
  conversation_id = row[0]
252
261
  conversation_title = row[1]
253
- description = await row[2].read() # Oracle.AsyncLOB
262
+ if row[2]:
263
+ description = await row[2].read() # Oracle.AsyncLOB
264
+ else:
265
+ description = None
254
266
  retention_days = row[3]
255
267
  attributes = ConversationAttributes(
256
268
  title=conversation_title,
@@ -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
select_ai/db.py CHANGED
@@ -73,7 +73,7 @@ def is_connected() -> bool:
73
73
  return False
74
74
  try:
75
75
  return conn.ping() is None
76
- except oracledb.DatabaseError:
76
+ except (oracledb.DatabaseError, oracledb.InterfaceError):
77
77
  return False
78
78
 
79
79
 
@@ -87,7 +87,7 @@ async def async_is_connected() -> bool:
87
87
  return False
88
88
  try:
89
89
  return await conn.ping() is None
90
- except oracledb.DatabaseError:
90
+ except (oracledb.DatabaseError, oracledb.InterfaceError):
91
91
  return False
92
92
 
93
93
 
select_ai/profile.py CHANGED
@@ -44,7 +44,7 @@ class Profile(BaseProfile):
44
44
  :return: None
45
45
  :raises: oracledb.DatabaseError
46
46
  """
47
- if self.profile_name is not None:
47
+ if self.profile_name:
48
48
  profile_exists = False
49
49
  try:
50
50
  saved_attributes = self._get_attributes(
@@ -64,7 +64,7 @@ class Profile(BaseProfile):
64
64
  profile_name=self.profile_name
65
65
  )
66
66
  except ProfileNotFoundError:
67
- if self.attributes is None:
67
+ if self.attributes is None and self.description is None:
68
68
  raise
69
69
  else:
70
70
  if self.attributes is None:
@@ -78,20 +78,28 @@ class Profile(BaseProfile):
78
78
  )
79
79
  if self.replace or not profile_exists:
80
80
  self.create(replace=self.replace)
81
+ else: # profile name is None
82
+ if self.attributes is not None or self.description is not None:
83
+ raise ValueError(
84
+ "Attribute 'profile_name' cannot be empty or None"
85
+ )
81
86
 
82
87
  @staticmethod
83
- def _get_profile_description(profile_name) -> str:
88
+ def _get_profile_description(profile_name) -> Union[str, None]:
84
89
  """Get description of profile from USER_CLOUD_AI_PROFILES
85
90
 
86
91
  :param str profile_name:
87
- :return: str
92
+ :return: Union[str, None] profile description
88
93
  :raises: ProfileNotFoundError
89
94
  """
90
95
  with cursor() as cr:
91
96
  cr.execute(GET_USER_AI_PROFILE, profile_name=profile_name.upper())
92
97
  profile = cr.fetchone()
93
98
  if profile:
94
- return profile[1].read()
99
+ if profile[1] is not None:
100
+ return profile[1].read()
101
+ else:
102
+ return None
95
103
  else:
96
104
  raise ProfileNotFoundError(profile_name)
97
105
 
@@ -165,6 +173,11 @@ class Profile(BaseProfile):
165
173
  attributes
166
174
  :return: None
167
175
  """
176
+ if not isinstance(attributes, ProfileAttributes):
177
+ raise TypeError(
178
+ "'attributes' must be an object of type"
179
+ " select_ai.ProfileAttributes"
180
+ )
168
181
  self.attributes = attributes
169
182
  parameters = {
170
183
  "profile_name": self.profile_name,
@@ -182,7 +195,8 @@ class Profile(BaseProfile):
182
195
  :return: None
183
196
  :raises: oracledb.DatabaseError
184
197
  """
185
-
198
+ if self.attributes is None:
199
+ raise AttributeError("Profile attributes cannot be None")
186
200
  parameters = {
187
201
  "profile_name": self.profile_name,
188
202
  "attributes": self.attributes.json(),
@@ -199,7 +213,7 @@ class Profile(BaseProfile):
199
213
  except oracledb.DatabaseError as e:
200
214
  (error,) = e.args
201
215
  # If already exists and replace is True then drop and recreate
202
- if "already exists" in error.message.lower() and replace:
216
+ if error.code == 20046 and replace:
203
217
  self.delete(force=True)
204
218
  cr.callproc(
205
219
  "DBMS_CLOUD_AI.CREATE_PROFILE",
@@ -244,12 +258,12 @@ class Profile(BaseProfile):
244
258
  raise ProfileNotFoundError(profile_name=profile_name)
245
259
 
246
260
  @classmethod
247
- def list(cls, profile_name_pattern: str) -> Iterator["Profile"]:
261
+ def list(cls, profile_name_pattern: str = ".*") -> Iterator["Profile"]:
248
262
  """List AI Profiles saved in the database.
249
263
 
250
264
  :param str profile_name_pattern: Regular expressions can be used
251
265
  to specify a pattern. Function REGEXP_LIKE is used to perform the
252
- match
266
+ match. Default value is ".*" i.e. match all AI profiles.
253
267
 
254
268
  :return: Iterator[Profile]
255
269
  """
@@ -283,6 +297,8 @@ class Profile(BaseProfile):
283
297
  conversation_id for context-aware chats
284
298
  :return: Union[pandas.DataFrame, str]
285
299
  """
300
+ if not prompt:
301
+ raise ValueError("prompt cannot be empty or None")
286
302
  parameters = {
287
303
  "prompt": prompt,
288
304
  "action": action,
@@ -393,6 +409,13 @@ class Profile(BaseProfile):
393
409
  :raises: oracledb.DatabaseError
394
410
 
395
411
  """
412
+ if synthetic_data_attributes is None:
413
+ raise ValueError("Param 'synthetic_data_attributes' cannot be None")
414
+
415
+ if not isinstance(synthetic_data_attributes, SyntheticDataAttributes):
416
+ raise TypeError("'synthetic_data_attributes' must be an object "
417
+ "of type select_ai.SyntheticDataAttributes")
418
+
396
419
  keyword_parameters = synthetic_data_attributes.prepare()
397
420
  keyword_parameters["profile_name"] = self.profile_name
398
421
  with cursor() as cr:
select_ai/provider.py CHANGED
@@ -5,11 +5,19 @@
5
5
  # http://oss.oracle.com/licenses/upl.
6
6
  # -----------------------------------------------------------------------------
7
7
 
8
- from dataclasses import dataclass, fields
9
- from typing import Optional
8
+ from dataclasses import dataclass
9
+ from typing import List, Optional, Union
10
10
 
11
11
  from select_ai._abc import SelectAIDataClass
12
12
 
13
+ from .db import async_cursor, cursor
14
+ from .sql import (
15
+ DISABLE_AI_PROFILE_DOMAIN_FOR_USER,
16
+ ENABLE_AI_PROFILE_DOMAIN_FOR_USER,
17
+ GRANT_PRIVILEGES_TO_USER,
18
+ REVOKE_PRIVILEGES_FROM_USER,
19
+ )
20
+
13
21
  OPENAI = "openai"
14
22
  COHERE = "cohere"
15
23
  AZURE = "azure"
@@ -184,3 +192,97 @@ class AnthropicProvider(Provider):
184
192
 
185
193
  provider_name: str = ANTHROPIC
186
194
  provider_endpoint = "api.anthropic.com"
195
+
196
+
197
+ async def async_enable_provider(
198
+ users: Union[str, List[str]], provider_endpoint: str = None
199
+ ):
200
+ """
201
+ Async API to enable AI profile for database users.
202
+
203
+ This method grants execute privilege on the packages DBMS_CLOUD,
204
+ DBMS_CLOUD_AI and DBMS_CLOUD_PIPELINE. It also enables the database
205
+ user to invoke the AI Provider (LLM) endpoint
206
+
207
+ """
208
+ if isinstance(users, str):
209
+ users = [users]
210
+
211
+ async with async_cursor() as cr:
212
+ for user in users:
213
+ await cr.execute(GRANT_PRIVILEGES_TO_USER.format(user))
214
+ if provider_endpoint:
215
+ await cr.execute(
216
+ ENABLE_AI_PROFILE_DOMAIN_FOR_USER,
217
+ user=user,
218
+ host=provider_endpoint,
219
+ )
220
+
221
+
222
+ async def async_disable_provider(
223
+ users: Union[str, List[str]], provider_endpoint: str = None
224
+ ):
225
+ """
226
+ Async API to disable AI profile for database users
227
+
228
+ Disables AI provider for the user. This method revokes execute privilege
229
+ on the packages DBMS_CLOUD, DBMS_CLOUD_AI and DBMS_CLOUD_PIPELINE. It
230
+ also disables the user to invoke the AI Provider (LLM) endpoint
231
+ """
232
+ if isinstance(users, str):
233
+ users = [users]
234
+
235
+ async with async_cursor() as cr:
236
+ for user in users:
237
+ await cr.execute(REVOKE_PRIVILEGES_FROM_USER.format(user))
238
+ if provider_endpoint:
239
+ await cr.execute(
240
+ DISABLE_AI_PROFILE_DOMAIN_FOR_USER,
241
+ user=user,
242
+ host=provider_endpoint,
243
+ )
244
+
245
+
246
+ def enable_provider(
247
+ users: Union[str, List[str]], provider_endpoint: str = None
248
+ ):
249
+ """
250
+ Enables AI profile for the user. This method grants execute privilege
251
+ on the packages DBMS_CLOUD, DBMS_CLOUD_AI and DBMS_CLOUD_PIPELINE. It
252
+ also enables the user to invoke the AI Provider (LLM) endpoint
253
+ """
254
+ if isinstance(users, str):
255
+ users = [users]
256
+
257
+ with cursor() as cr:
258
+ for user in users:
259
+ cr.execute(GRANT_PRIVILEGES_TO_USER.format(user))
260
+ if provider_endpoint:
261
+ cr.execute(
262
+ ENABLE_AI_PROFILE_DOMAIN_FOR_USER,
263
+ user=user,
264
+ host=provider_endpoint,
265
+ )
266
+
267
+
268
+ def disable_provider(
269
+ users: Union[str, List[str]], provider_endpoint: str = None
270
+ ):
271
+ """
272
+ Disables AI provider for the user. This method revokes execute privilege
273
+ on the packages DBMS_CLOUD, DBMS_CLOUD_AI and DBMS_CLOUD_PIPELINE. It
274
+ also disables the user to invoke the AI(LLM) endpoint
275
+
276
+ """
277
+ if isinstance(users, str):
278
+ users = [users]
279
+
280
+ with cursor() as cr:
281
+ for user in users:
282
+ cr.execute(REVOKE_PRIVILEGES_FROM_USER.format(user))
283
+ if provider_endpoint:
284
+ cr.execute(
285
+ DISABLE_AI_PROFILE_DOMAIN_FOR_USER,
286
+ user=user,
287
+ host=provider_endpoint,
288
+ )
@@ -60,6 +60,11 @@ class SyntheticDataAttributes(SelectAIDataClass):
60
60
  record_count: Optional[int] = None
61
61
  user_prompt: Optional[str] = None
62
62
 
63
+ def __post_init__(self):
64
+ if self.params and not isinstance(self.params, SyntheticDataParams):
65
+ raise TypeError("'params' must be an object of"
66
+ " type SyntheticDataParams'")
67
+
63
68
  def dict(self, exclude_null=True):
64
69
  attributes = {}
65
70
  for k, v in self.__dict__.items():
select_ai/vector_index.py CHANGED
@@ -192,7 +192,7 @@ class VectorIndex(_BaseVectorIndex):
192
192
  except oracledb.DatabaseError as e:
193
193
  (error,) = e.args
194
194
  # If already exists and replace is True then drop and recreate
195
- if "already exists" in error.message.lower() and replace:
195
+ if error.code == 20048 and replace:
196
196
  self.delete(force=True)
197
197
  cr.callproc(
198
198
  "DBMS_CLOUD_AI.CREATE_VECTOR_INDEX",
@@ -309,12 +309,12 @@ class VectorIndex(_BaseVectorIndex):
309
309
  return self._get_attributes(self.index_name)
310
310
 
311
311
  @classmethod
312
- def list(cls, index_name_pattern: str) -> Iterator["VectorIndex"]:
312
+ def list(cls, index_name_pattern: str = ".*") -> Iterator["VectorIndex"]:
313
313
  """List Vector Indexes
314
314
 
315
315
  :param str index_name_pattern: Regular expressions can be used
316
316
  to specify a pattern. Function REGEXP_LIKE is used to perform the
317
- match
317
+ match. Default value is ".*" i.e. match all vector indexes.
318
318
 
319
319
  :return: Iterator[VectorIndex]
320
320
  """
@@ -325,7 +325,10 @@ class VectorIndex(_BaseVectorIndex):
325
325
  )
326
326
  for row in cr.fetchall():
327
327
  index_name = row[0]
328
- description = row[1].read() # Oracle.LOB
328
+ if row[1]:
329
+ description = row[1].read() # Oracle.LOB
330
+ else:
331
+ description = None
329
332
  attributes = cls._get_attributes(index_name=index_name)
330
333
  yield cls(
331
334
  index_name=index_name,
@@ -396,7 +399,7 @@ class AsyncVectorIndex(_BaseVectorIndex):
396
399
  except oracledb.DatabaseError as e:
397
400
  (error,) = e.args
398
401
  # If already exists and replace is True then drop and recreate
399
- if "already exists" in error.message.lower() and replace:
402
+ if error.code == 20048 and replace:
400
403
  await self.delete(force=True)
401
404
  await cr.callproc(
402
405
  "DBMS_CLOUD_AI.CREATE_VECTOR_INDEX",
@@ -515,13 +518,14 @@ class AsyncVectorIndex(_BaseVectorIndex):
515
518
 
516
519
  @classmethod
517
520
  async def list(
518
- cls, index_name_pattern: str
521
+ cls, index_name_pattern: str = ".*"
519
522
  ) -> AsyncGenerator[VectorIndex, None]:
520
523
  """List Vector Indexes.
521
524
 
522
525
  :param str index_name_pattern: Regular expressions can be used
523
526
  to specify a pattern. Function REGEXP_LIKE is used to perform the
524
- match
527
+ match. Default value is ".*" i.e. match all vector indexes.
528
+
525
529
  :return: AsyncGenerator[VectorIndex]
526
530
 
527
531
  """
@@ -533,7 +537,10 @@ class AsyncVectorIndex(_BaseVectorIndex):
533
537
  rows = await cr.fetchall()
534
538
  for row in rows:
535
539
  index_name = row[0]
536
- description = await row[1].read() # AsyncLOB
540
+ if row[1]:
541
+ description = await row[1].read() # AsyncLOB
542
+ else:
543
+ description = None
537
544
  attributes = await cls._get_attributes(index_name=index_name)
538
545
  yield VectorIndex(
539
546
  index_name=index_name,
select_ai/version.py CHANGED
@@ -5,4 +5,4 @@
5
5
  # http://oss.oracle.com/licenses/upl.
6
6
  # -----------------------------------------------------------------------------
7
7
 
8
- __version__ = "1.0.0.dev6"
8
+ __version__ = "1.0.0.dev8"
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: select_ai
3
+ Version: 1.0.0.dev8
4
+ Summary: Select AI for Python
5
+ Author-email: Abhishek Singh <abhishek.o.singh@oracle.com>
6
+ Maintainer-email: Abhishek Singh <abhishek.o.singh@oracle.com>
7
+ License-Expression: UPL-1.0
8
+ Keywords: oracle,select-ai,adbs,autonomous database serverless
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Natural Language :: English
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: Implementation :: CPython
20
+ Classifier: Topic :: Database
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE.txt
24
+ Requires-Dist: oracledb
25
+ Requires-Dist: pandas==2.2.3
26
+ Dynamic: license-file
27
+
28
+ # Select AI for Python
29
+
30
+
31
+ Select AI for Python enables you to ask questions of your database data using natural language (text-to-SQL), get generative AI responses using your trusted content (retrieval augmented generation), generate synthetic data using large language models, and other features – all from Python. With the general availability of Select AI Python, Python developers have access to the functionality of Select AI on Oracle Autonomous Database.
32
+
33
+ Select AI for Python enables you to leverage the broader Python ecosystem in combination with generative AI and database functionality - bridging the gap between the DBMS_CLOUD_AI PL/SQL package and Python's rich ecosystem. It provides intuitive objects and methods for AI model interaction.
34
+
35
+
36
+ ## Installation
37
+
38
+ Run
39
+ ```bash
40
+ python3 -m pip install select_ai
41
+ ```
42
+
43
+ ## Samples
44
+
45
+ Examples can be found in the [/samples][samples] directory
46
+
47
+ ### Basic Example
48
+
49
+ ```python
50
+ import select_ai
51
+
52
+ user = "<your_select_ai_user>"
53
+ password = "<your_select_ai_password>"
54
+ dsn = "<your_select_ai_db_connect_string>"
55
+
56
+ select_ai.connect(user=user, password=password, dsn=dsn)
57
+ profile = select_ai.Profile(profile_name="oci_ai_profile")
58
+ df = profile.run_sql(
59
+ prompt="How many promotions are there in the sh database?"
60
+ )
61
+ print(df.columns)
62
+ print(df)
63
+ ```
64
+
65
+ ### Async Example
66
+
67
+ ```python
68
+
69
+ import asyncio
70
+
71
+ import select_ai
72
+
73
+ user = "<your_select_ai_user>"
74
+ password = "<your_select_ai_password>"
75
+ dsn = "<your_select_ai_db_connect_string>"
76
+
77
+ # This example shows how to asynchronously run sql
78
+ async def main():
79
+ await select_ai.async_connect(user=user, password=password, dsn=dsn)
80
+ async_profile = await select_ai.AsyncProfile(
81
+ profile_name="async_oci_ai_profile",
82
+ )
83
+ # run_sql returns a pandas df
84
+ df = await async_profile.run_sql("How many promotions?")
85
+ print(df)
86
+
87
+ asyncio.run(main())
88
+
89
+ ```
90
+ ## Help
91
+
92
+ Questions can be asked in [GitHub Discussions][ghdiscussions].
93
+
94
+ Problem reports can be raised in [GitHub Issues][ghissues].
95
+
96
+ ## Contributing
97
+
98
+
99
+ This project welcomes contributions from the community. Before submitting a pull request, please [review our contribution guide](./CONTRIBUTING.md)
100
+
101
+ ## Security
102
+
103
+ Please consult the [security guide](./SECURITY.md) for our responsible security vulnerability disclosure process
104
+
105
+ ## License
106
+
107
+ Copyright (c) 2025 Oracle and/or its affiliates.
108
+
109
+ Released under the Universal Permissive License v1.0 as shown at
110
+ <https://oss.oracle.com/licenses/upl/>.
111
+
112
+
113
+ [ghdiscussions]: https://github.com/oracle/python-select-ai/discussions
114
+ [ghissues]: https://github.com/oracle/python-select-ai/issues
115
+ [samples]: https://github.com/oracle/python-select-ai/tree/main/samples
@@ -0,0 +1,21 @@
1
+ select_ai/__init__.py,sha256=5oVcMxHqKPnIqzsPntFrqrMrTQ86bDrJsG8ARXuxN7U,1460
2
+ select_ai/_abc.py,sha256=ccec6yu54w5QMix_EKc_BJ_0JatUDXKjfLW9KshOGak,2692
3
+ select_ai/_enums.py,sha256=U0UavuE4pbRY-0-Qb8z-F7KfxU0YUfOgUahaSjDi0dU,416
4
+ select_ai/action.py,sha256=s78uhi5HHCLbTN2S5tLDSswTLUl40FZ3nESLka8CfZk,592
5
+ select_ai/async_profile.py,sha256=3Ybsl-mKIxJDelIXcmGTBdzgBdkDacomTGMx0X0kSsw,18705
6
+ select_ai/base_profile.py,sha256=hY9thC-7pHvMwh01W0t7OzOAyG8Fi8Lnd0KhZ7ouVNs,7005
7
+ select_ai/conversation.py,sha256=fbYH7ROLwpyf1VzNtcrESG1S0_ZfLJ-uObbvbUATJUY,9622
8
+ select_ai/credential.py,sha256=cDIsiPgjbvckbOfb2LeoztPUsDO-6cJ_S8DzYT3pm4k,4220
9
+ select_ai/db.py,sha256=ipyXt5_wEXAQGl7PLVq82FBuePdA_e6b8ee_vhEAMks,4723
10
+ select_ai/errors.py,sha256=2T5eICWWjj7a907aRh7es5wTcvWm_wJiP8h0b3ipxVQ,2114
11
+ select_ai/profile.py,sha256=UH7aC9B5W2w4WmflX6N5FRWfSLwagXxgz62T9enpOgY,16059
12
+ select_ai/provider.py,sha256=Sv0b-6SQJEoasPsnyHcWg1W8gS8nKldUW-83lL4MDm8,8285
13
+ select_ai/sql.py,sha256=9yCdB1H_sYpjMLs-6SB8xf5-QpTwCAx4cIgIsElvly4,2786
14
+ select_ai/synthetic_data.py,sha256=-l4eHLsTQRYTgJdsscszSWJlo9p75rLFMpH4XIodllE,3303
15
+ select_ai/vector_index.py,sha256=C6a3jgZ6ffyel7fF5owHV_2G4T_E62pmGp1QXl28M4w,20475
16
+ select_ai/version.py,sha256=9koElJAsAO9YflC7ndP0-ddsM4LAmyrJUQVmOKjJXz0,348
17
+ select_ai-1.0.0.dev8.dist-info/licenses/LICENSE.txt,sha256=_0VqOxSjO1hu6JexZDVzqUXSmzH1A53EOfyiJzXTBKc,1840
18
+ select_ai-1.0.0.dev8.dist-info/METADATA,sha256=TfklSE5CPwcdIc-paAEVPvUIlUmX10TlDFYjhNe703E,3705
19
+ select_ai-1.0.0.dev8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ select_ai-1.0.0.dev8.dist-info/top_level.txt,sha256=u_QUAHDibro58Lvi_MU6a9Wc6VfQT8HEQm0cciMTP-c,10
21
+ select_ai-1.0.0.dev8.dist-info/RECORD,,
select_ai/admin.py DELETED
@@ -1,116 +0,0 @@
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 List, Mapping, Union
9
-
10
- import oracledb
11
-
12
- from .db import cursor
13
- from .sql import (
14
- DISABLE_AI_PROFILE_DOMAIN_FOR_USER,
15
- ENABLE_AI_PROFILE_DOMAIN_FOR_USER,
16
- GRANT_PRIVILEGES_TO_USER,
17
- REVOKE_PRIVILEGES_FROM_USER,
18
- )
19
-
20
- __all__ = [
21
- "create_credential",
22
- "disable_provider",
23
- "enable_provider",
24
- ]
25
-
26
-
27
- def create_credential(credential: Mapping, replace: bool = False):
28
- """
29
- Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL
30
-
31
- if replace is True, credential will be replaced if it "already exists"
32
-
33
- """
34
- valid_keys = {
35
- "credential_name",
36
- "username",
37
- "password",
38
- "user_ocid",
39
- "tenancy_ocid",
40
- "private_key",
41
- "fingerprint",
42
- "comments",
43
- }
44
- for k in credential.keys():
45
- if k.lower() not in valid_keys:
46
- raise ValueError(
47
- f"Invalid value {k}: {credential[k]} for credential object"
48
- )
49
-
50
- with cursor() as cr:
51
- try:
52
- cr.callproc(
53
- "DBMS_CLOUD.CREATE_CREDENTIAL", keyword_parameters=credential
54
- )
55
- except oracledb.DatabaseError as e:
56
- (error,) = e.args
57
- # If already exists and replace is True then drop and recreate
58
- if "already exists" in error.message.lower() and replace:
59
- cr.callproc(
60
- "DBMS_CLOUD.DROP_CREDENTIAL",
61
- keyword_parameters={
62
- "credential_name": credential["credential_name"]
63
- },
64
- )
65
- cr.callproc(
66
- "DBMS_CLOUD.CREATE_CREDENTIAL",
67
- keyword_parameters=credential,
68
- )
69
- else:
70
- raise
71
-
72
-
73
- def enable_provider(
74
- users: Union[str, List[str]], provider_endpoint: str = None
75
- ):
76
- """
77
- Enables AI profile for the user. This method grants execute privilege
78
- on the packages DBMS_CLOUD, DBMS_CLOUD_AI and DBMS_CLOUD_PIPELINE. It
79
- also enables the user to invoke the AI(LLM) endpoint hosted at a
80
- certain domain
81
- """
82
- if isinstance(users, str):
83
- users = [users]
84
-
85
- with cursor() as cr:
86
- for user in users:
87
- cr.execute(GRANT_PRIVILEGES_TO_USER.format(user))
88
- if provider_endpoint:
89
- cr.execute(
90
- ENABLE_AI_PROFILE_DOMAIN_FOR_USER,
91
- user=user,
92
- host=provider_endpoint,
93
- )
94
-
95
-
96
- def disable_provider(
97
- users: Union[str, List[str]], provider_endpoint: str = None
98
- ):
99
- """
100
- Disables AI provider for the user. This method revokes execute privilege
101
- on the packages DBMS_CLOUD, DBMS_CLOUD_AI and DBMS_CLOUD_PIPELINE. It
102
- also disables the user to invoke the AI(LLM) endpoint hosted at a
103
- certain domain
104
- """
105
- if isinstance(users, str):
106
- users = [users]
107
-
108
- with cursor() as cr:
109
- for user in users:
110
- cr.execute(REVOKE_PRIVILEGES_FROM_USER.format(user))
111
- if provider_endpoint:
112
- cr.execute(
113
- DISABLE_AI_PROFILE_DOMAIN_FOR_USER,
114
- user=user,
115
- host=provider_endpoint,
116
- )
@@ -1,25 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: select_ai
3
- Version: 1.0.0.dev6
4
- Summary: Python API for Select AI
5
- Author-email: Abhishek Singh <abhishek.o.singh@oracle.com>
6
- Maintainer-email: Abhishek Singh <abhishek.o.singh@oracle.com>
7
- License-Expression: UPL-1.0
8
- Keywords: oracle,select-ai,adbs,autonomous database serverless
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Natural Language :: English
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python :: 3 :: Only
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Classifier: Programming Language :: Python :: Implementation :: CPython
20
- Classifier: Topic :: Database
21
- Requires-Python: >=3.9
22
- License-File: LICENSE.txt
23
- Requires-Dist: oracledb
24
- Requires-Dist: pandas==2.2.3
25
- Dynamic: license-file
@@ -1,21 +0,0 @@
1
- select_ai/__init__.py,sha256=LVdOh_JnWEXaXs0NSflA1f0YIWCUEpTBZCvWNTvSS74,1319
2
- select_ai/_abc.py,sha256=ccec6yu54w5QMix_EKc_BJ_0JatUDXKjfLW9KshOGak,2692
3
- select_ai/_enums.py,sha256=U0UavuE4pbRY-0-Qb8z-F7KfxU0YUfOgUahaSjDi0dU,416
4
- select_ai/action.py,sha256=s78uhi5HHCLbTN2S5tLDSswTLUl40FZ3nESLka8CfZk,592
5
- select_ai/admin.py,sha256=MMkXpvVJZUoloty8HiQnndPVPpuZch22Pnk8llUhUCc,3528
6
- select_ai/async_profile.py,sha256=1_ds6Rej4m17cd5aaRM5LoPOIL0EL4OVYBULiCLj57M,17593
7
- select_ai/base_profile.py,sha256=Iyd5GdBNqEISiWCwYpGSKOGd394f-A5QgHPtuJ547CE,6791
8
- select_ai/conversation.py,sha256=jeZy7qtbnFTO9xTE3gNZLyxVI0Pnj_rpc0QZb1r-A00,9240
9
- select_ai/db.py,sha256=Q0MvoIBzE0fHP1d9oFjvnjfgKqdRk6ZCM6bERokcM3E,4669
10
- select_ai/errors.py,sha256=2T5eICWWjj7a907aRh7es5wTcvWm_wJiP8h0b3ipxVQ,2114
11
- select_ai/profile.py,sha256=lyMnS1ExRPYwS54OqlLpH3WwJxkj_epat7Z_nVDH3Os,14868
12
- select_ai/provider.py,sha256=7KNdU2BmUMzjPj2WAwtn0vEYPBYY83MvRTT1q2Ng5tM,5103
13
- select_ai/sql.py,sha256=9yCdB1H_sYpjMLs-6SB8xf5-QpTwCAx4cIgIsElvly4,2786
14
- select_ai/synthetic_data.py,sha256=lnjEIcUzr7lr2dO24OgxRhol-tiVOiKK-kEvFVJoiig,3078
15
- select_ai/vector_index.py,sha256=y6Vrky7T5qNJK_eDVBS3U7i5B-Hvq6FBUd_CjkN23bo,20212
16
- select_ai/version.py,sha256=4HksrDhF_YLN3qkAVlYmmmBl9tDnJJ1XbM3x2xA-XfM,348
17
- select_ai-1.0.0.dev6.dist-info/licenses/LICENSE.txt,sha256=_0VqOxSjO1hu6JexZDVzqUXSmzH1A53EOfyiJzXTBKc,1840
18
- select_ai-1.0.0.dev6.dist-info/METADATA,sha256=P_89Il0dtluoQWFYZpexYhvfs18iymZT05A0hI3cKe4,1016
19
- select_ai-1.0.0.dev6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- select_ai-1.0.0.dev6.dist-info/top_level.txt,sha256=u_QUAHDibro58Lvi_MU6a9Wc6VfQT8HEQm0cciMTP-c,10
21
- select_ai-1.0.0.dev6.dist-info/RECORD,,