select-ai 1.2.0rc3__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.
@@ -0,0 +1,1129 @@
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 (
12
+ Any,
13
+ AsyncGenerator,
14
+ Iterator,
15
+ List,
16
+ Mapping,
17
+ Optional,
18
+ Union,
19
+ )
20
+
21
+ import oracledb
22
+
23
+ from select_ai import BaseProfile
24
+ from select_ai._abc import SelectAIDataClass
25
+ from select_ai._enums import StrEnum
26
+ from select_ai.agent.sql import (
27
+ GET_USER_AI_AGENT_TOOL,
28
+ GET_USER_AI_AGENT_TOOL_ATTRIBUTES,
29
+ LIST_USER_AI_AGENT_TOOLS,
30
+ )
31
+ from select_ai.async_profile import AsyncProfile
32
+ from select_ai.db import async_cursor, cursor
33
+ from select_ai.errors import AgentToolNotFoundError
34
+ from select_ai.profile import Profile
35
+
36
+
37
+ class NotificationType(StrEnum):
38
+ """
39
+ Notification Types
40
+ """
41
+
42
+ SLACK = "slack"
43
+ EMAIL = "email"
44
+
45
+
46
+ class ToolType(StrEnum):
47
+ """
48
+ Built-in Tool Types
49
+ """
50
+
51
+ EMAIL = "EMAIL"
52
+ HUMAN = "HUMAN"
53
+ HTTP = "HTTP"
54
+ RAG = "RAG"
55
+ SQL = "SQL"
56
+ SLACK = "SLACK"
57
+ WEBSEARCH = "WEBSEARCH"
58
+
59
+
60
+ @dataclass
61
+ class ToolParams(SelectAIDataClass):
62
+ """
63
+ Parameters to register a built-in Tool
64
+
65
+ :param str credential_name: Used by SLACK, EMAIL and WEBSEARCH tools
66
+
67
+ :param str endpoint: Send HTTP requests to this endpoint
68
+
69
+ :param select_ai.agent.NotificationType: Either SLACK or EMAIL
70
+
71
+ :param str profile_name: Name of AI profile to use
72
+
73
+ :param str recipient: Recipient used for EMAIL notification
74
+
75
+ :param str sender: Sender used for EMAIL notification
76
+
77
+ :param str slack_channel: Slack channel to use
78
+
79
+ :param str smtp_host: SMTP host to use for EMAIL notification
80
+
81
+ """
82
+
83
+ _REQUIRED_FIELDS: Optional[List] = None
84
+
85
+ credential_name: Optional[str] = None
86
+ endpoint: Optional[str] = None
87
+ notification_type: Optional[NotificationType] = None
88
+ profile_name: Optional[str] = None
89
+ recipient: Optional[str] = None
90
+ sender: Optional[str] = None
91
+ slack_channel: Optional[str] = None
92
+ smtp_host: Optional[str] = None
93
+
94
+ def __post_init__(self):
95
+ super().__post_init__()
96
+ if self._REQUIRED_FIELDS:
97
+ for field in self._REQUIRED_FIELDS:
98
+ if getattr(self, field) is None:
99
+ raise AttributeError(
100
+ "Required field '{}' not found.".format(field)
101
+ )
102
+
103
+ @classmethod
104
+ def create(cls, *, tool_type: Optional[ToolType] = None, **kwargs):
105
+ tool_params_cls = ToolTypeParams.get(tool_type, ToolParams)
106
+ return tool_params_cls(**kwargs)
107
+
108
+ @classmethod
109
+ def keys(cls):
110
+ return {
111
+ "credential_name",
112
+ "endpoint",
113
+ "notification_type",
114
+ "profile_name",
115
+ "recipient",
116
+ "sender",
117
+ "slack_channel",
118
+ "smtp_host",
119
+ }
120
+
121
+
122
+ @dataclass
123
+ class SQLToolParams(ToolParams):
124
+
125
+ _REQUIRED_FIELDS = ["profile_name"]
126
+
127
+
128
+ @dataclass
129
+ class RAGToolParams(ToolParams):
130
+
131
+ _REQUIRED_FIELDS = ["profile_name"]
132
+
133
+
134
+ @dataclass
135
+ class SlackNotificationToolParams(ToolParams):
136
+
137
+ _REQUIRED_FIELDS = ["credential_name", "slack_channel"]
138
+ notification_type: NotificationType = NotificationType.SLACK
139
+
140
+
141
+ @dataclass
142
+ class EmailNotificationToolParams(ToolParams):
143
+
144
+ _REQUIRED_FIELDS = ["credential_name", "recipient", "sender", "smtp_host"]
145
+ notification_type: NotificationType = NotificationType.EMAIL
146
+
147
+
148
+ @dataclass
149
+ class WebSearchToolParams(ToolParams):
150
+
151
+ _REQUIRED_FIELDS = ["credential_name"]
152
+
153
+
154
+ @dataclass
155
+ class HumanToolParams(ToolParams):
156
+ pass
157
+
158
+
159
+ @dataclass
160
+ class HTTPToolParams(ToolParams):
161
+
162
+ _REQUIRED_FIELDS = ["credential_name", "endpoint"]
163
+
164
+
165
+ @dataclass
166
+ class ToolAttributes(SelectAIDataClass):
167
+ """
168
+ AI Tool attributes
169
+
170
+ :param str instruction: Statement that describes what the tool
171
+ should accomplish and how to do it. This text is included
172
+ in the prompt sent to the LLM.
173
+ :param function: Specifies the PL/SQL procedure or
174
+ function to call when the tool is used
175
+ :param select_ai.agent.ToolParams tool_params: Tool parameters
176
+ for built-in tools
177
+ :param List[Mapping] tool_inputs: Describes input arguments.
178
+ Similar to column comments in a table. For example:
179
+ "tool_inputs": [
180
+ {
181
+ "name": "data_guard",
182
+ "description": "Only supported values are "Enabled" and "Disabled""
183
+ }
184
+ ]
185
+
186
+ """
187
+
188
+ instruction: Optional[str] = None
189
+ function: Optional[str] = None
190
+ tool_params: Optional[ToolParams] = None
191
+ tool_inputs: Optional[List[Mapping]] = None
192
+ tool_type: Optional[ToolType] = None
193
+
194
+ def dict(self, exclude_null=True):
195
+ attributes = {}
196
+ for k, v in self.__dict__.items():
197
+ if v is not None or not exclude_null:
198
+ if isinstance(v, ToolParams):
199
+ attributes[k] = v.dict(exclude_null=exclude_null)
200
+ else:
201
+ attributes[k] = v
202
+ return attributes
203
+
204
+ @classmethod
205
+ def create(cls, **kwargs):
206
+ tool_attributes = {}
207
+ tool_params = {}
208
+ for k, v in kwargs.items():
209
+ if isinstance(v, oracledb.LOB):
210
+ v = v.read()
211
+ if k in ToolParams.keys():
212
+ tool_params[k] = v
213
+ elif k == "tool_params" and v is not None:
214
+ tool_params = json.loads(v)
215
+ else:
216
+ tool_attributes[k] = v
217
+ tool_params = ToolParams.create(
218
+ tool_type=tool_attributes.get("tool_type"), **tool_params
219
+ )
220
+ tool_attributes["tool_params"] = tool_params
221
+ return ToolAttributes(**tool_attributes)
222
+
223
+
224
+ ToolTypeParams = {
225
+ ToolType.EMAIL: EmailNotificationToolParams,
226
+ ToolType.SLACK: SlackNotificationToolParams,
227
+ ToolType.HTTP: HTTPToolParams,
228
+ ToolType.RAG: RAGToolParams,
229
+ ToolType.SQL: SQLToolParams,
230
+ ToolType.WEBSEARCH: WebSearchToolParams,
231
+ ToolType.HUMAN: HumanToolParams,
232
+ }
233
+
234
+
235
+ class _BaseTool(ABC):
236
+
237
+ def __init__(
238
+ self,
239
+ tool_name: Optional[str] = None,
240
+ description: Optional[str] = None,
241
+ attributes: Optional[ToolAttributes] = None,
242
+ ):
243
+ """Initialize an AI Agent Tool"""
244
+ if attributes and not isinstance(attributes, ToolAttributes):
245
+ raise TypeError(
246
+ "'attributes' must be an object of type "
247
+ "select_ai.agent.ToolAttributes"
248
+ )
249
+ self.tool_name = tool_name
250
+ self.attributes = attributes
251
+ self.description = description
252
+
253
+ def __repr__(self):
254
+ return (
255
+ f"{self.__class__.__name__}("
256
+ f"tool_name={self.tool_name}, "
257
+ f"attributes={self.attributes}, description={self.description})"
258
+ )
259
+
260
+
261
+ class Tool(_BaseTool):
262
+
263
+ @staticmethod
264
+ def _get_attributes(tool_name: str) -> ToolAttributes:
265
+ """Get attributes of an AI tool
266
+
267
+ :return: select_ai.agent.ToolAttributes
268
+ :raises: AgentToolNotFoundError
269
+ """
270
+ with cursor() as cr:
271
+ cr.execute(
272
+ GET_USER_AI_AGENT_TOOL_ATTRIBUTES, tool_name=tool_name.upper()
273
+ )
274
+ attributes = cr.fetchall()
275
+ if attributes:
276
+ post_processed_attributes = {}
277
+ for k, v in attributes:
278
+ if isinstance(v, oracledb.LOB):
279
+ post_processed_attributes[k] = v.read()
280
+ else:
281
+ post_processed_attributes[k] = v
282
+ return ToolAttributes.create(**post_processed_attributes)
283
+ else:
284
+ raise AgentToolNotFoundError(tool_name=tool_name)
285
+
286
+ @staticmethod
287
+ def _get_description(tool_name: str) -> Union[str, None]:
288
+ with cursor() as cr:
289
+ cr.execute(GET_USER_AI_AGENT_TOOL, tool_name=tool_name.upper())
290
+ tool = cr.fetchone()
291
+ if tool:
292
+ if tool[1] is not None:
293
+ return tool[1].read()
294
+ else:
295
+ return None
296
+ else:
297
+ raise AgentToolNotFoundError(tool_name=tool_name)
298
+
299
+ def create(
300
+ self, enabled: Optional[bool] = True, replace: Optional[bool] = False
301
+ ):
302
+ if self.tool_name is None:
303
+ raise AttributeError("Tool must have a name")
304
+ if self.attributes is None:
305
+ raise AttributeError("Tool must have attributes")
306
+
307
+ parameters = {
308
+ "tool_name": self.tool_name,
309
+ "attributes": self.attributes.json(),
310
+ }
311
+ if self.description:
312
+ parameters["description"] = self.description
313
+
314
+ if not enabled:
315
+ parameters["status"] = "disabled"
316
+
317
+ with cursor() as cr:
318
+ try:
319
+ cr.callproc(
320
+ "DBMS_CLOUD_AI_AGENT.CREATE_TOOL",
321
+ keyword_parameters=parameters,
322
+ )
323
+ except oracledb.Error as err:
324
+ (err_obj,) = err.args
325
+ if err_obj.code in (20050, 20052) and replace:
326
+ self.delete(force=True)
327
+ cr.callproc(
328
+ "DBMS_CLOUD_AI_AGENT.CREATE_TOOL",
329
+ keyword_parameters=parameters,
330
+ )
331
+ else:
332
+ raise
333
+
334
+ @classmethod
335
+ def create_built_in_tool(
336
+ cls,
337
+ tool_name: str,
338
+ tool_params: ToolParams,
339
+ tool_type: ToolType,
340
+ description: Optional[str] = None,
341
+ replace: Optional[bool] = False,
342
+ ) -> "Tool":
343
+ """
344
+ Register a built-in tool
345
+
346
+ :param str tool_name: The name of the tool
347
+ :param select_ai.agent.ToolParams tool_params:
348
+ Parameters required by built-in tool
349
+ :param select_ai.agent.ToolType tool_type: The built-in tool type
350
+ :param str description: Description of the tool
351
+ :param bool replace: Whether to replace the existing tool.
352
+ Default value is False
353
+
354
+ :return: select_ai.agent.Tool
355
+ """
356
+ if not isinstance(tool_params, ToolParams):
357
+ raise TypeError(
358
+ "'tool_params' must be an object of "
359
+ "type select_ai.agent.ToolParams"
360
+ )
361
+ attributes = ToolAttributes(
362
+ tool_params=tool_params, tool_type=tool_type
363
+ )
364
+ tool = cls(
365
+ tool_name=tool_name, attributes=attributes, description=description
366
+ )
367
+ tool.create(replace=replace)
368
+ return tool
369
+
370
+ @classmethod
371
+ def create_email_notification_tool(
372
+ cls,
373
+ tool_name: str,
374
+ credential_name: str,
375
+ recipient: str,
376
+ sender: str,
377
+ smtp_host: str,
378
+ description: Optional[str],
379
+ replace: bool = False,
380
+ ) -> "Tool":
381
+ """
382
+ Register an email notification tool
383
+
384
+ :param str tool_name: The name of the tool
385
+ :param str credential_name: The name of the credential
386
+ :param str recipient: The recipient of the email
387
+ :param str sender: The sender of the email
388
+ :param str smtp_host: The SMTP host of the email server
389
+ :param str description: The description of the tool
390
+ :param bool replace: Whether to replace the existing tool.
391
+ Default value is False
392
+
393
+ :return: select_ai.agent.Tool
394
+
395
+ """
396
+ email_notification_tool_params = EmailNotificationToolParams(
397
+ credential_name=credential_name,
398
+ recipient=recipient,
399
+ sender=sender,
400
+ smtp_host=smtp_host,
401
+ )
402
+ return cls.create_built_in_tool(
403
+ tool_name=tool_name,
404
+ tool_type=ToolType.EMAIL,
405
+ tool_params=email_notification_tool_params,
406
+ description=description,
407
+ replace=replace,
408
+ )
409
+
410
+ @classmethod
411
+ def create_http_tool(
412
+ cls,
413
+ tool_name: str,
414
+ credential_name: str,
415
+ endpoint: str,
416
+ description: Optional[str] = None,
417
+ replace: bool = False,
418
+ ) -> "Tool":
419
+ http_tool_params = HTTPToolParams(
420
+ credential_name=credential_name, endpoint=endpoint
421
+ )
422
+ return cls.create_built_in_tool(
423
+ tool_name=tool_name,
424
+ tool_type=ToolType.HTTP,
425
+ tool_params=http_tool_params,
426
+ description=description,
427
+ replace=replace,
428
+ )
429
+
430
+ @classmethod
431
+ def create_pl_sql_tool(
432
+ cls,
433
+ tool_name: str,
434
+ function: str,
435
+ description: Optional[str] = None,
436
+ replace: bool = False,
437
+ ) -> "Tool":
438
+ """
439
+ Create a custom tool to invoke PL/SQL procedure or function
440
+
441
+ :param str tool_name: The name of the tool
442
+ :param str function: The name of the PL/SQL procedure or function
443
+ :param str description: The description of the tool
444
+ :param bool replace: Whether to replace existing tool. Default value
445
+ is False
446
+
447
+ """
448
+ tool_attributes = ToolAttributes(function=function)
449
+ tool = cls(
450
+ tool_name=tool_name,
451
+ attributes=tool_attributes,
452
+ description=description,
453
+ )
454
+ tool.create(replace=replace)
455
+ return tool
456
+
457
+ @classmethod
458
+ def create_rag_tool(
459
+ cls,
460
+ tool_name: str,
461
+ profile_name: str,
462
+ description: Optional[str] = None,
463
+ replace: bool = False,
464
+ ) -> "Tool":
465
+ """
466
+ Register a RAG tool, which will use a VectorIndex linked AI Profile
467
+
468
+ :param str tool_name: The name of the tool
469
+ :param str profile_name: The name of the profile to
470
+ use for Vector Index based RAG
471
+ :param str description: The description of the tool
472
+ :param bool replace: Whether to replace existing tool. Default value
473
+ is False
474
+ """
475
+ tool_params = RAGToolParams(profile_name=profile_name)
476
+ return cls.create_built_in_tool(
477
+ tool_name=tool_name,
478
+ tool_type=ToolType.RAG,
479
+ tool_params=tool_params,
480
+ description=description,
481
+ replace=replace,
482
+ )
483
+
484
+ @classmethod
485
+ def create_sql_tool(
486
+ cls,
487
+ tool_name: str,
488
+ profile_name: str,
489
+ description: Optional[str] = None,
490
+ replace: bool = False,
491
+ ) -> "Tool":
492
+ """
493
+ Register a SQL tool to perform natural language to SQL translation
494
+
495
+ :param str tool_name: The name of the tool
496
+ :param str profile_name: The name of the profile to use for SQL
497
+ translation
498
+ :param str description: The description of the tool
499
+ :param bool replace: Whether to replace existing tool. Default value
500
+ is False
501
+ """
502
+ tool_params = SQLToolParams(profile_name=profile_name)
503
+ return cls.create_built_in_tool(
504
+ tool_name=tool_name,
505
+ tool_type=ToolType.SQL,
506
+ tool_params=tool_params,
507
+ description=description,
508
+ replace=replace,
509
+ )
510
+
511
+ @classmethod
512
+ def create_slack_notification_tool(
513
+ cls,
514
+ tool_name: str,
515
+ credential_name: str,
516
+ slack_channel: str,
517
+ description: Optional[str] = None,
518
+ replace: bool = False,
519
+ ) -> "Tool":
520
+ """
521
+ Register a Slack notification tool
522
+
523
+ :param str tool_name: The name of the Slack notification tool
524
+ :param str credential_name: The name of the Slack credential
525
+ :param str slack_channel: The name of the Slack channel
526
+ :param str description: The description of the Slack notification tool
527
+ :param bool replace: Whether to replace existing tool. Default value
528
+ is False
529
+
530
+ """
531
+ slack_notification_tool_params = SlackNotificationToolParams(
532
+ credential_name=credential_name,
533
+ slack_channel=slack_channel,
534
+ )
535
+ return cls.create_built_in_tool(
536
+ tool_name=tool_name,
537
+ tool_type=ToolType.SLACK,
538
+ tool_params=slack_notification_tool_params,
539
+ description=description,
540
+ replace=replace,
541
+ )
542
+
543
+ @classmethod
544
+ def create_websearch_tool(
545
+ cls,
546
+ tool_name: str,
547
+ credential_name: str,
548
+ description: Optional[str],
549
+ replace: bool = False,
550
+ ) -> "Tool":
551
+ """
552
+ Register a built-in websearch tool to search information
553
+ on the web
554
+
555
+ :param str tool_name: The name of the tool
556
+ :param str credential_name: The name of the credential object
557
+ storing OpenAI credentials
558
+ :param str description: The description of the tool
559
+ :param bool replace: Whether to replace the existing tool
560
+
561
+ """
562
+ web_search_tool_params = WebSearchToolParams(
563
+ credential_name=credential_name,
564
+ )
565
+ return cls.create_built_in_tool(
566
+ tool_name=tool_name,
567
+ tool_type=ToolType.WEBSEARCH,
568
+ tool_params=web_search_tool_params,
569
+ description=description,
570
+ replace=replace,
571
+ )
572
+
573
+ def delete(self, force: bool = False):
574
+ """
575
+ Delete AI Tool from the database
576
+
577
+ :param bool force: Force the deletion. Default value is False.
578
+ """
579
+ with cursor() as cr:
580
+ cr.callproc(
581
+ "DBMS_CLOUD_AI_AGENT.DROP_TOOL",
582
+ keyword_parameters={
583
+ "tool_name": self.tool_name,
584
+ "force": force,
585
+ },
586
+ )
587
+
588
+ def disable(self):
589
+ """
590
+ Disable AI Tool
591
+ """
592
+ with cursor() as cr:
593
+ cr.callproc(
594
+ "DBMS_CLOUD_AI_AGENT.DISABLE_TOOL",
595
+ keyword_parameters={
596
+ "tool_name": self.tool_name,
597
+ },
598
+ )
599
+
600
+ def enable(self):
601
+ """
602
+ Enable AI Tool
603
+ """
604
+ with cursor() as cr:
605
+ cr.callproc(
606
+ "DBMS_CLOUD_AI_AGENT.ENABLE_TOOL",
607
+ keyword_parameters={
608
+ "tool_name": self.tool_name,
609
+ },
610
+ )
611
+
612
+ @classmethod
613
+ def fetch(cls, tool_name: str) -> "Tool":
614
+ """
615
+ Fetch AI Tool attributes from the Database and build a proxy object in
616
+ the Python layer
617
+
618
+ :param str tool_name: The name of the AI Task
619
+
620
+ :return: select_ai.agent.Tool
621
+
622
+ :raises select_ai.errors.AgentToolNotFoundError:
623
+ If the AI Tool is not found
624
+
625
+ """
626
+ attributes = cls._get_attributes(tool_name)
627
+ description = cls._get_description(tool_name)
628
+ return cls(
629
+ tool_name=tool_name, attributes=attributes, description=description
630
+ )
631
+
632
+ @classmethod
633
+ def list(cls, tool_name_pattern: str = ".*") -> Iterator["Tool"]:
634
+ """List AI Tools
635
+
636
+ :param str tool_name_pattern: Regular expressions can be used
637
+ to specify a pattern. Function REGEXP_LIKE is used to perform the
638
+ match. Default value is ".*" i.e. match all tool name.
639
+
640
+ :return: Iterator[Tool]
641
+ """
642
+ with cursor() as cr:
643
+ cr.execute(
644
+ LIST_USER_AI_AGENT_TOOLS,
645
+ tool_name_pattern=tool_name_pattern,
646
+ )
647
+ for row in cr.fetchall():
648
+ tool_name = row[0]
649
+ if row[1]:
650
+ description = row[1].read() # Oracle.LOB
651
+ else:
652
+ description = None
653
+ attributes = cls._get_attributes(tool_name=tool_name)
654
+ yield cls(
655
+ tool_name=tool_name,
656
+ description=description,
657
+ attributes=attributes,
658
+ )
659
+
660
+ def set_attributes(self, attributes: ToolAttributes) -> None:
661
+ """
662
+ Set the attributes of the AI Agent tool
663
+ """
664
+ parameters = {
665
+ "object_name": self.tool_name,
666
+ "object_type": "tool",
667
+ "attributes": attributes.json(),
668
+ }
669
+ with cursor() as cr:
670
+ cr.callproc(
671
+ "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES",
672
+ keyword_parameters=parameters,
673
+ )
674
+
675
+ def set_attribute(self, attribute_name: str, attribute_value: Any) -> None:
676
+ """
677
+ Set the attribute of the AI Agent tool specified by
678
+ `attribute_name` and `attribute_value`.
679
+ """
680
+ parameters = {
681
+ "object_name": self.tool_name,
682
+ "object_type": "tool",
683
+ "attribute_name": attribute_name,
684
+ "attribute_value": attribute_value,
685
+ }
686
+ with cursor() as cr:
687
+ cr.callproc(
688
+ "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE",
689
+ keyword_parameters=parameters,
690
+ )
691
+
692
+
693
+ class AsyncTool(_BaseTool):
694
+
695
+ @staticmethod
696
+ async def _get_attributes(tool_name: str) -> ToolAttributes:
697
+ """Get attributes of an AI tool
698
+
699
+ :return: select_ai.agent.ToolAttributes
700
+ :raises: AgentToolNotFoundError
701
+ """
702
+ async with async_cursor() as cr:
703
+ await cr.execute(
704
+ GET_USER_AI_AGENT_TOOL_ATTRIBUTES, tool_name=tool_name.upper()
705
+ )
706
+ attributes = await cr.fetchall()
707
+ if attributes:
708
+ post_processed_attributes = {}
709
+ for k, v in attributes:
710
+ if isinstance(v, oracledb.AsyncLOB):
711
+ post_processed_attributes[k] = await v.read()
712
+ else:
713
+ post_processed_attributes[k] = v
714
+ return ToolAttributes.create(**post_processed_attributes)
715
+ else:
716
+ raise AgentToolNotFoundError(tool_name=tool_name)
717
+
718
+ @staticmethod
719
+ async def _get_description(tool_name: str) -> Union[str, None]:
720
+ async with async_cursor() as cr:
721
+ await cr.execute(
722
+ GET_USER_AI_AGENT_TOOL, tool_name=tool_name.upper()
723
+ )
724
+ tool = await cr.fetchone()
725
+ if tool:
726
+ if tool[1] is not None:
727
+ return await tool[1].read()
728
+ else:
729
+ return None
730
+ else:
731
+ raise AgentToolNotFoundError(tool_name=tool_name)
732
+
733
+ async def create(
734
+ self, enabled: Optional[bool] = True, replace: Optional[bool] = False
735
+ ):
736
+ if self.tool_name is None:
737
+ raise AttributeError("Tool must have a name")
738
+ if self.attributes is None:
739
+ raise AttributeError("Tool must have attributes")
740
+
741
+ parameters = {
742
+ "tool_name": self.tool_name,
743
+ "attributes": self.attributes.json(),
744
+ }
745
+ if self.description:
746
+ parameters["description"] = self.description
747
+
748
+ if not enabled:
749
+ parameters["status"] = "disabled"
750
+
751
+ async with async_cursor() as cr:
752
+ try:
753
+ await cr.callproc(
754
+ "DBMS_CLOUD_AI_AGENT.CREATE_TOOL",
755
+ keyword_parameters=parameters,
756
+ )
757
+ except oracledb.Error as err:
758
+ (err_obj,) = err.args
759
+ if err_obj.code in (20050, 20052) and replace:
760
+ await self.delete(force=True)
761
+ await cr.callproc(
762
+ "DBMS_CLOUD_AI_AGENT.CREATE_TOOL",
763
+ keyword_parameters=parameters,
764
+ )
765
+ else:
766
+ raise
767
+
768
+ @classmethod
769
+ async def create_built_in_tool(
770
+ cls,
771
+ tool_name: str,
772
+ tool_params: ToolParams,
773
+ tool_type: ToolType,
774
+ description: Optional[str] = None,
775
+ replace: Optional[bool] = False,
776
+ ) -> "AsyncTool":
777
+ """
778
+ Register a built-in tool
779
+
780
+ :param str tool_name: The name of the tool
781
+ :param select_ai.agent.ToolParams tool_params:
782
+ Parameters required by built-in tool
783
+ :param select_ai.agent.ToolType tool_type: The built-in tool type
784
+ :param str description: Description of the tool
785
+ :param bool replace: Whether to replace the existing tool.
786
+ Default value is False
787
+
788
+ :return: select_ai.agent.Tool
789
+ """
790
+ if not isinstance(tool_params, ToolParams):
791
+ raise TypeError(
792
+ "'tool_params' must be an object of "
793
+ "type select_ai.agent.ToolParams"
794
+ )
795
+ attributes = ToolAttributes(
796
+ tool_params=tool_params, tool_type=tool_type
797
+ )
798
+ tool = cls(
799
+ tool_name=tool_name, attributes=attributes, description=description
800
+ )
801
+ await tool.create(replace=replace)
802
+ return tool
803
+
804
+ @classmethod
805
+ async def create_email_notification_tool(
806
+ cls,
807
+ tool_name: str,
808
+ credential_name: str,
809
+ recipient: str,
810
+ sender: str,
811
+ smtp_host: str,
812
+ description: Optional[str],
813
+ replace: bool = False,
814
+ ) -> "AsyncTool":
815
+ """
816
+ Register an email notification tool
817
+
818
+ :param str tool_name: The name of the tool
819
+ :param str credential_name: The name of the credential
820
+ :param str recipient: The recipient of the email
821
+ :param str sender: The sender of the email
822
+ :param str smtp_host: The SMTP host of the email server
823
+ :param str description: The description of the tool
824
+ :param bool replace: Whether to replace the existing tool.
825
+ Default value is False
826
+
827
+ :return: select_ai.agent.Tool
828
+
829
+ """
830
+ email_notification_tool_params = EmailNotificationToolParams(
831
+ credential_name=credential_name,
832
+ recipient=recipient,
833
+ sender=sender,
834
+ smtp_host=smtp_host,
835
+ )
836
+ return await cls.create_built_in_tool(
837
+ tool_name=tool_name,
838
+ tool_type=ToolType.EMAIL,
839
+ tool_params=email_notification_tool_params,
840
+ description=description,
841
+ replace=replace,
842
+ )
843
+
844
+ @classmethod
845
+ async def create_http_tool(
846
+ cls,
847
+ tool_name: str,
848
+ credential_name: str,
849
+ endpoint: str,
850
+ description: Optional[str] = None,
851
+ replace: bool = False,
852
+ ) -> "AsyncTool":
853
+ http_tool_params = HTTPToolParams(
854
+ credential_name=credential_name, endpoint=endpoint
855
+ )
856
+ return await cls.create_built_in_tool(
857
+ tool_name=tool_name,
858
+ tool_type=ToolType.HTTP,
859
+ tool_params=http_tool_params,
860
+ description=description,
861
+ replace=replace,
862
+ )
863
+
864
+ @classmethod
865
+ async def create_pl_sql_tool(
866
+ cls,
867
+ tool_name: str,
868
+ function: str,
869
+ description: Optional[str] = None,
870
+ replace: bool = False,
871
+ ) -> "AsyncTool":
872
+ """
873
+ Create a custom tool to invoke PL/SQL procedure or function
874
+
875
+ :param str tool_name: The name of the tool
876
+ :param str function: The name of the PL/SQL procedure or function
877
+ :param str description: The description of the tool
878
+ :param bool replace: Whether to replace existing tool. Default value
879
+ is False
880
+
881
+ """
882
+ tool_attributes = ToolAttributes(function=function)
883
+ tool = cls(
884
+ tool_name=tool_name,
885
+ attributes=tool_attributes,
886
+ description=description,
887
+ )
888
+ await tool.create(replace=replace)
889
+ return tool
890
+
891
+ @classmethod
892
+ async def create_rag_tool(
893
+ cls,
894
+ tool_name: str,
895
+ profile_name: str,
896
+ description: Optional[str] = None,
897
+ replace: bool = False,
898
+ ) -> "AsyncTool":
899
+ """
900
+ Register a RAG tool, which will use a VectorIndex linked AI Profile
901
+
902
+ :param str tool_name: The name of the tool
903
+ :param str profile_name: The name of the profile to
904
+ use for Vector Index based RAG
905
+ :param str description: The description of the tool
906
+ :param bool replace: Whether to replace existing tool. Default value
907
+ is False
908
+ """
909
+ tool_params = RAGToolParams(profile_name=profile_name)
910
+ return await cls.create_built_in_tool(
911
+ tool_name=tool_name,
912
+ tool_type=ToolType.RAG,
913
+ tool_params=tool_params,
914
+ description=description,
915
+ replace=replace,
916
+ )
917
+
918
+ @classmethod
919
+ async def create_sql_tool(
920
+ cls,
921
+ tool_name: str,
922
+ profile_name: str,
923
+ description: Optional[str] = None,
924
+ replace: bool = False,
925
+ ) -> "AsyncTool":
926
+ """
927
+ Register a SQL tool to perform natural language to SQL translation
928
+
929
+ :param str tool_name: The name of the tool
930
+ :param str profile_name: The name of the profile to use for SQL
931
+ translation
932
+ :param str description: The description of the tool
933
+ :param bool replace: Whether to replace existing tool. Default value
934
+ is False
935
+ """
936
+ tool_params = SQLToolParams(profile_name=profile_name)
937
+ return await cls.create_built_in_tool(
938
+ tool_name=tool_name,
939
+ tool_type=ToolType.SQL,
940
+ tool_params=tool_params,
941
+ description=description,
942
+ replace=replace,
943
+ )
944
+
945
+ @classmethod
946
+ async def create_slack_notification_tool(
947
+ cls,
948
+ tool_name: str,
949
+ credential_name: str,
950
+ slack_channel: str,
951
+ description: Optional[str] = None,
952
+ replace: bool = False,
953
+ ) -> "AsyncTool":
954
+ """
955
+ Register a Slack notification tool
956
+
957
+ :param str tool_name: The name of the Slack notification tool
958
+ :param str credential_name: The name of the Slack credential
959
+ :param str slack_channel: The name of the Slack channel
960
+ :param str description: The description of the Slack notification tool
961
+ :param bool replace: Whether to replace existing tool. Default value
962
+ is False
963
+
964
+ """
965
+ slack_notification_tool_params = SlackNotificationToolParams(
966
+ credential_name=credential_name,
967
+ slack_channel=slack_channel,
968
+ )
969
+ return await cls.create_built_in_tool(
970
+ tool_name=tool_name,
971
+ tool_type=ToolType.SLACK,
972
+ tool_params=slack_notification_tool_params,
973
+ description=description,
974
+ replace=replace,
975
+ )
976
+
977
+ @classmethod
978
+ async def create_websearch_tool(
979
+ cls,
980
+ tool_name: str,
981
+ credential_name: str,
982
+ description: Optional[str],
983
+ replace: bool = False,
984
+ ) -> "AsyncTool":
985
+ """
986
+ Register a built-in websearch tool to search information
987
+ on the web
988
+
989
+ :param str tool_name: The name of the tool
990
+ :param str credential_name: The name of the credential object
991
+ storing OpenAI credentials
992
+ :param str description: The description of the tool
993
+ :param bool replace: Whether to replace the existing tool
994
+
995
+ """
996
+ web_search_tool_params = WebSearchToolParams(
997
+ credential_name=credential_name,
998
+ )
999
+ return await cls.create_built_in_tool(
1000
+ tool_name=tool_name,
1001
+ tool_type=ToolType.WEBSEARCH,
1002
+ tool_params=web_search_tool_params,
1003
+ description=description,
1004
+ replace=replace,
1005
+ )
1006
+
1007
+ async def delete(self, force: bool = False):
1008
+ """
1009
+ Delete AI Tool from the database
1010
+
1011
+ :param bool force: Force the deletion. Default value is False.
1012
+ """
1013
+ async with async_cursor() as cr:
1014
+ await cr.callproc(
1015
+ "DBMS_CLOUD_AI_AGENT.DROP_TOOL",
1016
+ keyword_parameters={
1017
+ "tool_name": self.tool_name,
1018
+ "force": force,
1019
+ },
1020
+ )
1021
+
1022
+ async def disable(self):
1023
+ """
1024
+ Disable AI Tool
1025
+ """
1026
+ async with async_cursor() as cr:
1027
+ await cr.callproc(
1028
+ "DBMS_CLOUD_AI_AGENT.DISABLE_TOOL",
1029
+ keyword_parameters={
1030
+ "tool_name": self.tool_name,
1031
+ },
1032
+ )
1033
+
1034
+ async def enable(self):
1035
+ """
1036
+ Enable AI Tool
1037
+ """
1038
+ async with async_cursor() as cr:
1039
+ await cr.callproc(
1040
+ "DBMS_CLOUD_AI_AGENT.ENABLE_TOOL",
1041
+ keyword_parameters={
1042
+ "tool_name": self.tool_name,
1043
+ },
1044
+ )
1045
+
1046
+ @classmethod
1047
+ async def fetch(cls, tool_name: str) -> "AsyncTool":
1048
+ """
1049
+ Fetch AI Tool attributes from the Database and build a proxy object in
1050
+ the Python layer
1051
+
1052
+ :param str tool_name: The name of the AI Task
1053
+
1054
+ :return: select_ai.agent.Tool
1055
+
1056
+ :raises select_ai.errors.AgentToolNotFoundError:
1057
+ If the AI Tool is not found
1058
+
1059
+ """
1060
+ attributes = await cls._get_attributes(tool_name)
1061
+ description = await cls._get_description(tool_name)
1062
+ return cls(
1063
+ tool_name=tool_name, attributes=attributes, description=description
1064
+ )
1065
+
1066
+ @classmethod
1067
+ async def list(
1068
+ cls, tool_name_pattern: str = ".*"
1069
+ ) -> AsyncGenerator["AsyncTool", None]:
1070
+ """List AI Tools
1071
+
1072
+ :param str tool_name_pattern: Regular expressions can be used
1073
+ to specify a pattern. Function REGEXP_LIKE is used to perform the
1074
+ match. Default value is ".*" i.e. match all tool name.
1075
+
1076
+ :return: Iterator[Tool]
1077
+ """
1078
+ async with async_cursor() as cr:
1079
+ await cr.execute(
1080
+ LIST_USER_AI_AGENT_TOOLS,
1081
+ tool_name_pattern=tool_name_pattern,
1082
+ )
1083
+ rows = await cr.fetchall()
1084
+ for row in rows:
1085
+ tool_name = row[0]
1086
+ if row[1]:
1087
+ description = await row[1].read() # Oracle.AsyncLOB
1088
+ else:
1089
+ description = None
1090
+ attributes = await cls._get_attributes(tool_name=tool_name)
1091
+ yield cls(
1092
+ tool_name=tool_name,
1093
+ description=description,
1094
+ attributes=attributes,
1095
+ )
1096
+
1097
+ async def set_attributes(self, attributes: ToolAttributes) -> None:
1098
+ """
1099
+ Set the attributes of the AI Agent tool
1100
+ """
1101
+ parameters = {
1102
+ "object_name": self.tool_name,
1103
+ "object_type": "tool",
1104
+ "attributes": attributes.json(),
1105
+ }
1106
+ async with async_cursor() as cr:
1107
+ await cr.callproc(
1108
+ "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES",
1109
+ keyword_parameters=parameters,
1110
+ )
1111
+
1112
+ async def set_attribute(
1113
+ self, attribute_name: str, attribute_value: Any
1114
+ ) -> None:
1115
+ """
1116
+ Set the attribute of the AI Agent tool specified by
1117
+ `attribute_name` and `attribute_value`.
1118
+ """
1119
+ parameters = {
1120
+ "object_name": self.tool_name,
1121
+ "object_type": "tool",
1122
+ "attribute_name": attribute_name,
1123
+ "attribute_value": attribute_value,
1124
+ }
1125
+ async with async_cursor() as cr:
1126
+ await cr.callproc(
1127
+ "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE",
1128
+ keyword_parameters=parameters,
1129
+ )