select-ai 1.1.0rc1__py3-none-any.whl → 1.2.0rc1__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,620 @@
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 AsyncGenerator, Iterator, List, Mapping, Optional, Union
12
+
13
+ import oracledb
14
+
15
+ from select_ai import BaseProfile
16
+ from select_ai._abc import SelectAIDataClass
17
+ from select_ai._enums import StrEnum
18
+ from select_ai.agent.sql import (
19
+ GET_USER_AI_AGENT_TOOL_ATTRIBUTES,
20
+ LIST_USER_AI_AGENT_TOOLS,
21
+ )
22
+ from select_ai.async_profile import AsyncProfile
23
+ from select_ai.db import async_cursor, cursor
24
+ from select_ai.errors import AgentToolNotFoundError
25
+ from select_ai.profile import Profile
26
+
27
+
28
+ class NotificationType(StrEnum):
29
+ """
30
+ Notification Types
31
+ """
32
+
33
+ SLACK = "slack"
34
+ EMAIL = "email"
35
+
36
+
37
+ class ToolType(StrEnum):
38
+ """
39
+ Built-in Tool Types
40
+ """
41
+
42
+ EMAIL = "EMAIL"
43
+ HUMAN = "HUMAN"
44
+ HTTP = "HTTP"
45
+ RAG = "RAG"
46
+ SQL = "SQL"
47
+ SLACK = "SLACK"
48
+ WEBSEARCH = "WEBSEARCH"
49
+
50
+
51
+ @dataclass
52
+ class ToolParams(SelectAIDataClass):
53
+ """
54
+ Parameters to register a built-in Tool
55
+
56
+ :param str credential_name: Used by SLACK, EMAIL and WEBSEARCH tools
57
+
58
+ :param str endpoint: Send HTTP requests to this endpoint
59
+
60
+ :param select_ai.agent.NotificationType: Either SLACK or EMAIL
61
+
62
+ :param str profile_name: Name of AI profile to use
63
+
64
+ :param str recipient: Recipient used for EMAIL notification
65
+
66
+ :param str sender: Sender used for EMAIL notification
67
+
68
+ :param str slack_channel: Slack channel to use
69
+
70
+ :param str smtp_host: SMTP host to use for EMAIL notification
71
+
72
+ """
73
+
74
+ _REQUIRED_FIELDS: Optional[List] = None
75
+
76
+ credential_name: Optional[str] = None
77
+ endpoint: Optional[str] = None
78
+ notification_type: Optional[NotificationType] = None
79
+ profile_name: Optional[str] = None
80
+ recipient: Optional[str] = None
81
+ sender: Optional[str] = None
82
+ slack_channel: Optional[str] = None
83
+ smtp_host: Optional[str] = None
84
+
85
+ def __post_init__(self):
86
+ super().__post_init__()
87
+ if self._REQUIRED_FIELDS:
88
+ for field in self._REQUIRED_FIELDS:
89
+ if getattr(self, field) is None:
90
+ raise AttributeError(
91
+ "Required field '{}' not found.".format(field)
92
+ )
93
+
94
+ @classmethod
95
+ def create(cls, *, tool_type: Optional[ToolType] = None, **kwargs):
96
+ tool_params_cls = ToolTypeParams.get(tool_type, ToolParams)
97
+ return tool_params_cls(**kwargs)
98
+
99
+ @classmethod
100
+ def keys(cls):
101
+ return {
102
+ "credential_name",
103
+ "endpoint",
104
+ "notification_type",
105
+ "profile_name",
106
+ "recipient",
107
+ "sender",
108
+ "slack_channel",
109
+ "smtp_host",
110
+ }
111
+
112
+
113
+ @dataclass
114
+ class SQLToolParams(ToolParams):
115
+
116
+ _REQUIRED_FIELDS = ["profile_name"]
117
+
118
+
119
+ @dataclass
120
+ class RAGToolParams(ToolParams):
121
+
122
+ _REQUIRED_FIELDS = ["profile_name"]
123
+
124
+
125
+ @dataclass
126
+ class SlackNotificationToolParams(ToolParams):
127
+
128
+ _REQUIRED_FIELDS = ["credential_name", "slack_channel"]
129
+ notification_type: NotificationType = NotificationType.SLACK
130
+
131
+
132
+ @dataclass
133
+ class EmailNotificationToolParams(ToolParams):
134
+
135
+ _REQUIRED_FIELDS = ["credential_name", "recipient", "sender", "smtp_host"]
136
+ notification_type: NotificationType = NotificationType.EMAIL
137
+
138
+
139
+ @dataclass
140
+ class WebSearchToolParams(ToolParams):
141
+
142
+ _REQUIRED_FIELDS = ["credential_name"]
143
+
144
+
145
+ @dataclass
146
+ class HumanToolParams(ToolParams):
147
+ pass
148
+
149
+
150
+ @dataclass
151
+ class HTTPToolParams(ToolParams):
152
+
153
+ _REQUIRED_FIELDS = ["credential_name", "endpoint"]
154
+
155
+
156
+ @dataclass
157
+ class ToolAttributes(SelectAIDataClass):
158
+ """
159
+ AI Tool attributes
160
+
161
+ :param str instruction: Statement that describes what the tool
162
+ should accomplish and how to do it. This text is included
163
+ in the prompt sent to the LLM.
164
+ :param function: Specifies the PL/SQL procedure or
165
+ function to call when the tool is used
166
+ :param select_ai.agent.ToolParams tool_params: Tool parameters
167
+ for built-in tools
168
+ :param List[Mapping] tool_inputs: Describes input arguments.
169
+ Similar to column comments in a table. For example:
170
+ "tool_inputs": [
171
+ {
172
+ "name": "data_guard",
173
+ "description": "Only supported values are "Enabled" and "Disabled""
174
+ }
175
+ ]
176
+
177
+ """
178
+
179
+ instruction: Optional[str] = None
180
+ function: Optional[str] = None
181
+ tool_params: Optional[ToolParams] = None
182
+ tool_inputs: Optional[List[Mapping]] = None
183
+ tool_type: Optional[ToolType] = None
184
+
185
+ def dict(self, exclude_null=True):
186
+ attributes = {}
187
+ for k, v in self.__dict__.items():
188
+ if v is not None or not exclude_null:
189
+ if isinstance(v, ToolParams):
190
+ attributes[k] = v.dict(exclude_null=exclude_null)
191
+ else:
192
+ attributes[k] = v
193
+ return attributes
194
+
195
+ @classmethod
196
+ def create(cls, **kwargs):
197
+ tool_attributes = {}
198
+ tool_params = {}
199
+ for k, v in kwargs.items():
200
+ if isinstance(v, oracledb.LOB):
201
+ v = v.read()
202
+ if k in ToolParams.keys():
203
+ tool_params[k] = v
204
+ elif k == "tool_params" and v is not None:
205
+ tool_params = json.loads(v)
206
+ else:
207
+ tool_attributes[k] = v
208
+ tool_params = ToolParams.create(
209
+ tool_type=tool_attributes.get("tool_type"), **tool_params
210
+ )
211
+ tool_attributes["tool_params"] = tool_params
212
+ return ToolAttributes(**tool_attributes)
213
+
214
+
215
+ ToolTypeParams = {
216
+ ToolType.EMAIL: EmailNotificationToolParams,
217
+ ToolType.SLACK: SlackNotificationToolParams,
218
+ ToolType.HTTP: HTTPToolParams,
219
+ ToolType.RAG: RAGToolParams,
220
+ ToolType.SQL: SQLToolParams,
221
+ ToolType.WEBSEARCH: WebSearchToolParams,
222
+ ToolType.HUMAN: HumanToolParams,
223
+ }
224
+
225
+
226
+ class _BaseTool(ABC):
227
+
228
+ def __init__(
229
+ self,
230
+ tool_name: Optional[str] = None,
231
+ description: Optional[str] = None,
232
+ attributes: Optional[ToolAttributes] = None,
233
+ ):
234
+ """Initialize an AI Agent Tool"""
235
+ if attributes and not isinstance(attributes, ToolAttributes):
236
+ raise TypeError(
237
+ "'attributes' must be an object of type "
238
+ "select_ai.agent.ToolAttributes"
239
+ )
240
+ self.tool_name = tool_name
241
+ self.attributes = attributes
242
+ self.description = description
243
+
244
+ def __repr__(self):
245
+ return (
246
+ f"{self.__class__.__name__}("
247
+ f"tool_name={self.tool_name}, "
248
+ f"attributes={self.attributes}, description={self.description})"
249
+ )
250
+
251
+
252
+ class Tool(_BaseTool):
253
+
254
+ @staticmethod
255
+ def _get_attributes(tool_name: str) -> ToolAttributes:
256
+ """Get attributes of an AI tool
257
+
258
+ :return: select_ai.agent.ToolAttributes
259
+ :raises: AgentToolNotFoundError
260
+ """
261
+ with cursor() as cr:
262
+ cr.execute(
263
+ GET_USER_AI_AGENT_TOOL_ATTRIBUTES, tool_name=tool_name.upper()
264
+ )
265
+ attributes = cr.fetchall()
266
+ if attributes:
267
+ post_processed_attributes = {}
268
+ for k, v in attributes:
269
+ if isinstance(v, oracledb.LOB):
270
+ post_processed_attributes[k] = v.read()
271
+ else:
272
+ post_processed_attributes[k] = v
273
+ return ToolAttributes.create(**post_processed_attributes)
274
+ else:
275
+ raise AgentToolNotFoundError(tool_name=tool_name)
276
+
277
+ def create(
278
+ self, enabled: Optional[bool] = True, replace: Optional[bool] = False
279
+ ):
280
+ if self.tool_name is None:
281
+ raise AttributeError("Tool must have a name")
282
+ if self.attributes is None:
283
+ raise AttributeError("Tool must have attributes")
284
+
285
+ parameters = {
286
+ "tool_name": self.tool_name,
287
+ "attributes": self.attributes.json(),
288
+ }
289
+ if self.description:
290
+ parameters["description"] = self.description
291
+
292
+ if not enabled:
293
+ parameters["status"] = "disabled"
294
+
295
+ with cursor() as cr:
296
+ try:
297
+ cr.callproc(
298
+ "DBMS_CLOUD_AI_AGENT.CREATE_TOOL",
299
+ keyword_parameters=parameters,
300
+ )
301
+ except oracledb.Error as err:
302
+ (err_obj,) = err.args
303
+ if err_obj.code in (20050, 20052) and replace:
304
+ self.delete(force=True)
305
+ cr.callproc(
306
+ "DBMS_CLOUD_AI_AGENT.CREATE_TOOL",
307
+ keyword_parameters=parameters,
308
+ )
309
+ else:
310
+ raise
311
+
312
+ @classmethod
313
+ def create_built_in_tool(
314
+ cls,
315
+ tool_name: str,
316
+ tool_params: ToolParams,
317
+ tool_type: ToolType,
318
+ description: Optional[str] = None,
319
+ replace: Optional[bool] = False,
320
+ ) -> "Tool":
321
+ """
322
+ Register a built-in tool
323
+
324
+ :param str tool_name: The name of the tool
325
+ :param select_ai.agent.ToolParams tool_params:
326
+ Parameters required by built-in tool
327
+ :param select_ai.agent.ToolType tool_type: The built-in tool type
328
+ :param str description: Description of the tool
329
+ :param bool replace: Whether to replace the existing tool.
330
+ Default value is False
331
+
332
+ :return: select_ai.agent.Tool
333
+ """
334
+ if not isinstance(tool_params, ToolParams):
335
+ raise TypeError(
336
+ "'tool_params' must be an object of "
337
+ "type select_ai.agent.ToolParams"
338
+ )
339
+ attributes = ToolAttributes(
340
+ tool_params=tool_params, tool_type=tool_type
341
+ )
342
+ tool = cls(
343
+ tool_name=tool_name, attributes=attributes, description=description
344
+ )
345
+ tool.create(replace=replace)
346
+ return tool
347
+
348
+ @classmethod
349
+ def create_email_notification_tool(
350
+ cls,
351
+ tool_name: str,
352
+ credential_name: str,
353
+ recipient: str,
354
+ sender: str,
355
+ smtp_host: str,
356
+ description: Optional[str],
357
+ replace: bool = False,
358
+ ) -> "Tool":
359
+ """
360
+ Register an email notification tool
361
+
362
+ :param str tool_name: The name of the tool
363
+ :param str credential_name: The name of the credential
364
+ :param str recipient: The recipient of the email
365
+ :param str sender: The sender of the email
366
+ :param str smtp_host: The SMTP host of the email server
367
+ :param str description: The description of the tool
368
+ :param bool replace: Whether to replace the existing tool.
369
+ Default value is False
370
+
371
+ :return: select_ai.agent.Tool
372
+
373
+ """
374
+ email_notification_tool_params = EmailNotificationToolParams(
375
+ credential_name=credential_name,
376
+ recipient=recipient,
377
+ sender=sender,
378
+ smtp_host=smtp_host,
379
+ )
380
+ return cls.create_built_in_tool(
381
+ tool_name=tool_name,
382
+ tool_type=ToolType.EMAIL,
383
+ tool_params=email_notification_tool_params,
384
+ description=description,
385
+ replace=replace,
386
+ )
387
+
388
+ @classmethod
389
+ def create_http_tool(
390
+ cls,
391
+ tool_name: str,
392
+ credential_name: str,
393
+ endpoint: str,
394
+ description: Optional[str] = None,
395
+ replace: bool = False,
396
+ ) -> "Tool":
397
+ http_tool_params = HTTPToolParams(
398
+ credential_name=credential_name, endpoint=endpoint
399
+ )
400
+ return cls.create_built_in_tool(
401
+ tool_name=tool_name,
402
+ tool_type=ToolType.HTTP,
403
+ tool_params=http_tool_params,
404
+ description=description,
405
+ replace=replace,
406
+ )
407
+
408
+ @classmethod
409
+ def create_pl_sql_tool(
410
+ cls,
411
+ tool_name: str,
412
+ function: str,
413
+ description: Optional[str] = None,
414
+ replace: bool = False,
415
+ ) -> "Tool":
416
+ """
417
+ Create a custom tool to invoke PL/SQL procedure or function
418
+
419
+ :param str tool_name: The name of the tool
420
+ :param str function: The name of the PL/SQL procedure or function
421
+ :param str description: The description of the tool
422
+ :param bool replace: Whether to replace existing tool. Default value
423
+ is False
424
+
425
+ """
426
+ tool_attributes = ToolAttributes(function=function)
427
+ tool = cls(
428
+ tool_name=tool_name,
429
+ attributes=tool_attributes,
430
+ description=description,
431
+ )
432
+ tool.create(replace=replace)
433
+ return tool
434
+
435
+ @classmethod
436
+ def create_rag_tool(
437
+ cls,
438
+ tool_name: str,
439
+ profile_name: str,
440
+ description: Optional[str] = None,
441
+ replace: bool = False,
442
+ ) -> "Tool":
443
+ """
444
+ Register a RAG tool, which will use a VectorIndex linked AI Profile
445
+
446
+ :param str tool_name: The name of the tool
447
+ :param str profile_name: The name of the profile to
448
+ use for Vector Index based RAG
449
+ :param str description: The description of the tool
450
+ :param bool replace: Whether to replace existing tool. Default value
451
+ is False
452
+ """
453
+ tool_params = RAGToolParams(profile_name=profile_name)
454
+ return cls.create_built_in_tool(
455
+ tool_name=tool_name,
456
+ tool_type=ToolType.RAG,
457
+ tool_params=tool_params,
458
+ description=description,
459
+ replace=replace,
460
+ )
461
+
462
+ @classmethod
463
+ def create_sql_tool(
464
+ cls,
465
+ tool_name: str,
466
+ profile_name: str,
467
+ description: Optional[str] = None,
468
+ replace: bool = False,
469
+ ) -> "Tool":
470
+ """
471
+ Register a SQL tool to perform natural language to SQL translation
472
+
473
+ :param str tool_name: The name of the tool
474
+ :param str profile_name: The name of the profile to use for SQL
475
+ translation
476
+ :param str description: The description of the tool
477
+ :param bool replace: Whether to replace existing tool. Default value
478
+ is False
479
+ """
480
+ tool_params = SQLToolParams(profile_name=profile_name)
481
+ return cls.create_built_in_tool(
482
+ tool_name=tool_name,
483
+ tool_type=ToolType.SQL,
484
+ tool_params=tool_params,
485
+ description=description,
486
+ replace=replace,
487
+ )
488
+
489
+ @classmethod
490
+ def create_slack_notification_tool(
491
+ cls,
492
+ tool_name: str,
493
+ credential_name: str,
494
+ slack_channel: str,
495
+ description: Optional[str] = None,
496
+ replace: bool = False,
497
+ ) -> "Tool":
498
+ """
499
+ Register a Slack notification tool
500
+
501
+ :param str tool_name: The name of the Slack notification tool
502
+ :param str credential_name: The name of the Slack credential
503
+ :param str slack_channel: The name of the Slack channel
504
+ :param str description: The description of the Slack notification tool
505
+ :param bool replace: Whether to replace existing tool. Default value
506
+ is False
507
+
508
+ """
509
+ slack_notification_tool_params = SlackNotificationToolParams(
510
+ credential_name=credential_name,
511
+ slack_channel=slack_channel,
512
+ )
513
+ return cls.create_built_in_tool(
514
+ tool_name=tool_name,
515
+ tool_type=ToolType.SLACK,
516
+ tool_params=slack_notification_tool_params,
517
+ description=description,
518
+ replace=replace,
519
+ )
520
+
521
+ @classmethod
522
+ def create_websearch_tool(
523
+ cls,
524
+ tool_name: str,
525
+ credential_name: str,
526
+ description: Optional[str],
527
+ replace: bool = False,
528
+ ) -> "Tool":
529
+ """
530
+ Register a built-in websearch tool to search information
531
+ on the web
532
+
533
+ :param str tool_name: The name of the tool
534
+ :param str credential_name: The name of the credential object
535
+ storing OpenAI credentials
536
+ :param str description: The description of the tool
537
+ :param bool replace: Whether to replace the existing tool
538
+
539
+ """
540
+ web_search_tool_params = WebSearchToolParams(
541
+ credential_name=credential_name,
542
+ )
543
+ return cls.create_built_in_tool(
544
+ tool_name=tool_name,
545
+ tool_type=ToolType.WEBSEARCH,
546
+ tool_params=web_search_tool_params,
547
+ description=description,
548
+ replace=replace,
549
+ )
550
+
551
+ def disable(self):
552
+ """
553
+ Disable AI Tool
554
+ """
555
+ pass
556
+
557
+ def delete(self, force: bool = False):
558
+ """
559
+ Delete AI Tool from the database
560
+
561
+ :param bool force: Force the deletion. Default value is False.
562
+ """
563
+ with cursor() as cr:
564
+ cr.callproc(
565
+ "DBMS_CLOUD_AI_AGENT.DROP_TOOL",
566
+ keyword_parameters={
567
+ "tool_name": self.tool_name,
568
+ "force": force,
569
+ },
570
+ )
571
+
572
+ def enable(self):
573
+ """
574
+ Enable AI Tool
575
+ """
576
+ pass
577
+
578
+ @classmethod
579
+ def fetch(cls, tool_name: str) -> "Tool":
580
+ """
581
+ Fetch AI Tool attributes from the Database and build a proxy object in
582
+ the Python layer
583
+
584
+ :param str tool_name: The name of the AI Task
585
+
586
+ :return: select_ai.agent.Tool
587
+
588
+ :raises select_ai.errors.AgentToolNotFoundError:
589
+ If the AI Tool is not found
590
+
591
+ """
592
+ pass
593
+
594
+ @classmethod
595
+ def list(cls, tool_name_pattern: str = ".*") -> Iterator["Tool"]:
596
+ """List AI Tools
597
+
598
+ :param str tool_name_pattern: Regular expressions can be used
599
+ to specify a pattern. Function REGEXP_LIKE is used to perform the
600
+ match. Default value is ".*" i.e. match all tool name.
601
+
602
+ :return: Iterator[Tool]
603
+ """
604
+ with cursor() as cr:
605
+ cr.execute(
606
+ LIST_USER_AI_AGENT_TOOLS,
607
+ tool_name_pattern=tool_name_pattern,
608
+ )
609
+ for row in cr.fetchall():
610
+ tool_name = row[0]
611
+ if row[1]:
612
+ description = row[1].read() # Oracle.LOB
613
+ else:
614
+ description = None
615
+ attributes = cls._get_attributes(tool_name=tool_name)
616
+ yield cls(
617
+ tool_name=tool_name,
618
+ description=description,
619
+ attributes=attributes,
620
+ )
@@ -24,7 +24,7 @@ from select_ai.action import Action
24
24
  from select_ai.base_profile import (
25
25
  BaseProfile,
26
26
  ProfileAttributes,
27
- no_data_for_prompt
27
+ no_data_for_prompt,
28
28
  )
29
29
  from select_ai.conversation import AsyncConversation
30
30
  from select_ai.db import async_cursor, async_get_connection
select_ai/base_profile.py CHANGED
@@ -188,6 +188,6 @@ class BaseProfile(ABC):
188
188
  def no_data_for_prompt(result) -> bool:
189
189
  if result is None:
190
190
  return True
191
- if result == 'No data found for the prompt.':
191
+ if result == "No data found for the prompt.":
192
192
  return True
193
193
  return False
select_ai/errors.py CHANGED
@@ -71,3 +71,43 @@ class VectorIndexNotFoundError(SelectAIError):
71
71
  )
72
72
  else:
73
73
  return f"VectorIndex {self.index_name} not found"
74
+
75
+
76
+ class AgentNotFoundError(SelectAIError):
77
+ """Agent not found in the database"""
78
+
79
+ def __init__(self, agent_name: str):
80
+ self.agent_name = agent_name
81
+
82
+ def __str__(self):
83
+ return f"Agent {self.agent_name} not found"
84
+
85
+
86
+ class AgentTaskNotFoundError(SelectAIError):
87
+ """Agent task not found in the database"""
88
+
89
+ def __init__(self, task_name: str):
90
+ self.task_name = task_name
91
+
92
+ def __str__(self):
93
+ return f"Agent Task {self.task_name} not found"
94
+
95
+
96
+ class AgentToolNotFoundError(SelectAIError):
97
+ """Agent tool not found in the database"""
98
+
99
+ def __init__(self, tool_name: str):
100
+ self.tool_name = tool_name
101
+
102
+ def __str__(self):
103
+ return f"Agent Tool {self.tool_name} not found"
104
+
105
+
106
+ class AgentTeamNotFoundError(SelectAIError):
107
+ """Agent team not found in the database"""
108
+
109
+ def __init__(self, team_name: str):
110
+ self.team_name = team_name
111
+
112
+ def __str__(self):
113
+ return f"Agent Team {self.team_name} not found"
select_ai/profile.py CHANGED
@@ -18,7 +18,7 @@ from select_ai.action import Action
18
18
  from select_ai.base_profile import (
19
19
  BaseProfile,
20
20
  ProfileAttributes,
21
- no_data_for_prompt
21
+ no_data_for_prompt,
22
22
  )
23
23
  from select_ai.db import cursor
24
24
  from select_ai.errors import ProfileExistsError, ProfileNotFoundError
@@ -324,7 +324,7 @@ class Profile(BaseProfile):
324
324
  else:
325
325
  result = None
326
326
  if action == Action.RUNSQL:
327
- if no_data_for_prompt(result): # empty dataframe
327
+ if no_data_for_prompt(result): # empty dataframe
328
328
  return pandas.DataFrame()
329
329
  return pandas.DataFrame(json.loads(result))
330
330
  else: