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,590 @@
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_TEAM,
28
+ GET_USER_AI_AGENT_TEAM_ATTRIBUTES,
29
+ LIST_USER_AI_AGENT_TEAMS,
30
+ )
31
+ from select_ai.async_profile import AsyncProfile
32
+ from select_ai.db import async_cursor, cursor
33
+ from select_ai.errors import AgentTeamNotFoundError
34
+ from select_ai.profile import Profile
35
+
36
+
37
+ @dataclass
38
+ class TeamAttributes(SelectAIDataClass):
39
+ """
40
+ AI agent team attributes
41
+
42
+ :param List[Mapping] agents: A List of Python dictionaries, each defining
43
+ the agent and the task name. [{"name": "<agent_name>",
44
+ "task": "<task_name>"}]
45
+
46
+ :param str process: Execution order of tasks. Currently only "sequential"
47
+ is supported.
48
+
49
+ """
50
+
51
+ agents: List[Mapping]
52
+ process: str = "sequential"
53
+
54
+
55
+ class BaseTeam(ABC):
56
+
57
+ def __init__(
58
+ self,
59
+ team_name: str,
60
+ attributes: TeamAttributes,
61
+ description: Optional[str] = None,
62
+ ):
63
+ if not isinstance(attributes, TeamAttributes):
64
+ raise TypeError(
65
+ f"attributes must be an object of type "
66
+ f"select_ai.agent.TeamAttributes instance"
67
+ )
68
+ self.team_name = team_name
69
+ self.description = description
70
+ self.attributes = attributes
71
+
72
+ def __repr__(self):
73
+ return (
74
+ f"{self.__class__.__name__}("
75
+ f"team_name={self.team_name}, "
76
+ f"attributes={self.attributes}, description={self.description})"
77
+ )
78
+
79
+
80
+ class Team(BaseTeam):
81
+ """
82
+ A Team of AI agents work together to accomplish tasks
83
+ select_ai.agent.Team class lets you create, delete, enable, disable and
84
+ list AI Tasks.
85
+
86
+ :param str team_name: The name of the AI team
87
+ :param str description: Optional description of the AI team
88
+ :param select_ai.agent.TeamAttributes attributes: AI team attributes
89
+
90
+ """
91
+
92
+ @staticmethod
93
+ def _get_attributes(team_name: str) -> TeamAttributes:
94
+ with cursor() as cr:
95
+ cr.execute(
96
+ GET_USER_AI_AGENT_TEAM_ATTRIBUTES, team_name=team_name.upper()
97
+ )
98
+ attributes = cr.fetchall()
99
+ if attributes:
100
+ post_processed_attributes = {}
101
+ for k, v in attributes:
102
+ if isinstance(v, oracledb.LOB):
103
+ post_processed_attributes[k] = v.read()
104
+ else:
105
+ post_processed_attributes[k] = v
106
+ return TeamAttributes(**post_processed_attributes)
107
+ else:
108
+ raise AgentTeamNotFoundError(team_name=team_name)
109
+
110
+ @staticmethod
111
+ def _get_description(team_name: str) -> Union[str, None]:
112
+ with cursor() as cr:
113
+ cr.execute(GET_USER_AI_AGENT_TEAM, team_name=team_name.upper())
114
+ team = cr.fetchone()
115
+ if team:
116
+ if team[1] is not None:
117
+ return team[1].read()
118
+ else:
119
+ return None
120
+ else:
121
+ raise AgentTeamNotFoundError(team_name=team_name)
122
+
123
+ def create(
124
+ self, enabled: Optional[bool] = True, replace: Optional[bool] = False
125
+ ):
126
+ """
127
+ Create a team of AI agents that work together to accomplish tasks.
128
+
129
+ :param bool enabled: Whether the AI agent team should be enabled.
130
+ Default value is True.
131
+
132
+ :param bool replace: Whether the AI agent team should be replaced.
133
+ Default value is False.
134
+
135
+ """
136
+ if self.team_name is None:
137
+ raise AttributeError("Team must have a name")
138
+ if self.attributes is None:
139
+ raise AttributeError("Team must have attributes")
140
+
141
+ parameters = {
142
+ "team_name": self.team_name,
143
+ "attributes": self.attributes.json(),
144
+ }
145
+ if self.description:
146
+ parameters["description"] = self.description
147
+
148
+ if not enabled:
149
+ parameters["status"] = "disabled"
150
+
151
+ with cursor() as cr:
152
+ try:
153
+ cr.callproc(
154
+ "DBMS_CLOUD_AI_AGENT.CREATE_TEAM",
155
+ keyword_parameters=parameters,
156
+ )
157
+ except oracledb.Error as err:
158
+ (err_obj,) = err.args
159
+ if err_obj.code in (20053, 20052) and replace:
160
+ self.delete(force=True)
161
+ cr.callproc(
162
+ "DBMS_CLOUD_AI_AGENT.CREATE_TEAM",
163
+ keyword_parameters=parameters,
164
+ )
165
+ else:
166
+ raise
167
+
168
+ def delete(self, force: Optional[bool] = False):
169
+ """
170
+ Delete an AI agent team from the database
171
+
172
+ :param bool force: Force the deletion. Default value is False.
173
+ """
174
+ with cursor() as cr:
175
+ cr.callproc(
176
+ "DBMS_CLOUD_AI_AGENT.DROP_TEAM",
177
+ keyword_parameters={
178
+ "team_name": self.team_name,
179
+ "force": force,
180
+ },
181
+ )
182
+
183
+ def disable(self):
184
+ """
185
+ Disable the AI agent team
186
+ """
187
+ with cursor() as cr:
188
+ cr.callproc(
189
+ "DBMS_CLOUD_AI_AGENT.DISABLE_TEAM",
190
+ keyword_parameters={
191
+ "team_name": self.team_name,
192
+ },
193
+ )
194
+
195
+ def enable(self):
196
+ """
197
+ Enable the AI agent team
198
+ """
199
+ with cursor() as cr:
200
+ cr.callproc(
201
+ "DBMS_CLOUD_AI_AGENT.ENABLE_TEAM",
202
+ keyword_parameters={
203
+ "team_name": self.team_name,
204
+ },
205
+ )
206
+
207
+ @classmethod
208
+ def fetch(cls, team_name: str) -> "Team":
209
+ """
210
+ Fetch AI Team attributes from the Database and build a proxy object in
211
+ the Python layer
212
+
213
+ :param str team_name: The name of the AI Team
214
+
215
+ :return: select_ai.agent.Team
216
+
217
+ :raises select_ai.errors.AgentTeamNotFoundError:
218
+ If the AI Team is not found
219
+ """
220
+ attributes = cls._get_attributes(team_name)
221
+ description = cls._get_description(team_name)
222
+ return cls(
223
+ team_name=team_name,
224
+ attributes=attributes,
225
+ description=description,
226
+ )
227
+
228
+ @classmethod
229
+ def list(cls, team_name_pattern: Optional[str] = ".*") -> Iterator["Team"]:
230
+ """
231
+ List AI Agent Teams
232
+
233
+ :param str team_name_pattern: Regular expressions can be used
234
+ to specify a pattern. Function REGEXP_LIKE is used to perform the
235
+ match. Default value is ".*" i.e. match all teams.
236
+
237
+ :return: Iterator[Team]
238
+
239
+ """
240
+ with cursor() as cr:
241
+ cr.execute(
242
+ LIST_USER_AI_AGENT_TEAMS,
243
+ team_name_pattern=team_name_pattern,
244
+ )
245
+ for row in cr.fetchall():
246
+ team_name = row[0]
247
+ if row[1]:
248
+ description = row[1].read() # Oracle.LOB
249
+ else:
250
+ description = None
251
+ attributes = cls._get_attributes(team_name=team_name)
252
+ yield cls(
253
+ team_name=team_name,
254
+ description=description,
255
+ attributes=attributes,
256
+ )
257
+
258
+ def run(self, prompt: str = None, params: Mapping = None):
259
+ """
260
+ Start a new AI agent team or resume a paused one that is waiting
261
+ for human input. If you provide an existing process ID and the
262
+ associated team process is in the WAITING_FOR_HUMAN state, the
263
+ function resumes the workflow using the input you provide as
264
+ the human response
265
+
266
+ :param str prompt: Optional prompt for the user. If the task is
267
+ in the RUNNING state, the input acts as a placeholder for the
268
+ {query} in the task instruction. If the task is in the
269
+ WAITING_FOR_HUMAN state, the input serves as the human response.
270
+
271
+ :param Mapping[str, str] params: Optional parameters for the task.
272
+ Currently, the following parameters are supported:
273
+
274
+ - conversation_id: Identifies the conversation session associated
275
+ with the agent team
276
+
277
+ - variables: key-value pairs that provide additional input to the agent team.
278
+
279
+ """
280
+ parameters = {
281
+ "team_name": self.team_name,
282
+ }
283
+ if prompt:
284
+ parameters["user_prompt"] = prompt
285
+ if params:
286
+ parameters["params"] = json.dumps(params)
287
+
288
+ with cursor() as cr:
289
+ data = cr.callfunc(
290
+ "DBMS_CLOUD_AI_AGENT.RUN_TEAM",
291
+ oracledb.DB_TYPE_CLOB,
292
+ keyword_parameters=parameters,
293
+ )
294
+ if data is not None:
295
+ result = data.read()
296
+ else:
297
+ result = None
298
+ return result
299
+
300
+ def set_attributes(self, attributes: TeamAttributes) -> None:
301
+ """
302
+ Set the attributes of the AI Agent team
303
+ """
304
+ parameters = {
305
+ "object_name": self.team_name,
306
+ "object_type": "team",
307
+ "attributes": attributes.json(),
308
+ }
309
+ with cursor() as cr:
310
+ cr.callproc(
311
+ "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES",
312
+ keyword_parameters=parameters,
313
+ )
314
+
315
+ def set_attribute(self, attribute_name: str, attribute_value: Any) -> None:
316
+ """
317
+ Set the attribute of the AI Agent team specified by
318
+ `attribute_name` and `attribute_value`.
319
+ """
320
+ parameters = {
321
+ "object_name": self.team_name,
322
+ "object_type": "team",
323
+ "attribute_name": attribute_name,
324
+ "attribute_value": attribute_value,
325
+ }
326
+ with cursor() as cr:
327
+ cr.callproc(
328
+ "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE",
329
+ keyword_parameters=parameters,
330
+ )
331
+
332
+
333
+ class AsyncTeam(BaseTeam):
334
+ """
335
+ A Team of AI agents work together to accomplish tasks
336
+ select_ai.agent.Team class lets you create, delete, enable, disable and
337
+ list AI Tasks.
338
+
339
+ :param str team_name: The name of the AI team
340
+ :param str description: Optional description of the AI team
341
+ :param select_ai.agent.TeamAttributes attributes: AI team attributes
342
+
343
+ """
344
+
345
+ @staticmethod
346
+ async def _get_attributes(team_name: str) -> TeamAttributes:
347
+ async with async_cursor() as cr:
348
+ await cr.execute(
349
+ GET_USER_AI_AGENT_TEAM_ATTRIBUTES, team_name=team_name.upper()
350
+ )
351
+ attributes = await cr.fetchall()
352
+ if attributes:
353
+ post_processed_attributes = {}
354
+ for k, v in attributes:
355
+ if isinstance(v, oracledb.AsyncLOB):
356
+ post_processed_attributes[k] = await v.read()
357
+ else:
358
+ post_processed_attributes[k] = v
359
+ return TeamAttributes(**post_processed_attributes)
360
+ else:
361
+ raise AgentTeamNotFoundError(team_name=team_name)
362
+
363
+ @staticmethod
364
+ async def _get_description(team_name: str) -> Union[str, None]:
365
+ async with async_cursor() as cr:
366
+ await cr.execute(
367
+ GET_USER_AI_AGENT_TEAM, team_name=team_name.upper()
368
+ )
369
+ team = await cr.fetchone()
370
+ if team:
371
+ if team[1] is not None:
372
+ return await team[1].read()
373
+ else:
374
+ return None
375
+ else:
376
+ raise AgentTeamNotFoundError(team_name=team_name)
377
+
378
+ async def create(
379
+ self, enabled: Optional[bool] = True, replace: Optional[bool] = False
380
+ ):
381
+ """
382
+ Create a team of AI agents that work together to accomplish tasks.
383
+
384
+ :param bool enabled: Whether the AI agent team should be enabled.
385
+ Default value is True.
386
+
387
+ :param bool replace: Whether the AI agent team should be replaced.
388
+ Default value is False.
389
+
390
+ """
391
+ if self.team_name is None:
392
+ raise AttributeError("Team must have a name")
393
+ if self.attributes is None:
394
+ raise AttributeError("Team must have attributes")
395
+
396
+ parameters = {
397
+ "team_name": self.team_name,
398
+ "attributes": self.attributes.json(),
399
+ }
400
+ if self.description:
401
+ parameters["description"] = self.description
402
+
403
+ if not enabled:
404
+ parameters["status"] = "disabled"
405
+
406
+ async with async_cursor() as cr:
407
+ try:
408
+ await cr.callproc(
409
+ "DBMS_CLOUD_AI_AGENT.CREATE_TEAM",
410
+ keyword_parameters=parameters,
411
+ )
412
+ except oracledb.Error as err:
413
+ (err_obj,) = err.args
414
+ if err_obj.code in (20053, 20052) and replace:
415
+ await self.delete(force=True)
416
+ await cr.callproc(
417
+ "DBMS_CLOUD_AI_AGENT.CREATE_TEAM",
418
+ keyword_parameters=parameters,
419
+ )
420
+ else:
421
+ raise
422
+
423
+ async def delete(self, force: Optional[bool] = False):
424
+ """
425
+ Delete an AI agent team from the database
426
+
427
+ :param bool force: Force the deletion. Default value is False.
428
+ """
429
+ async with async_cursor() as cr:
430
+ await cr.callproc(
431
+ "DBMS_CLOUD_AI_AGENT.DROP_TEAM",
432
+ keyword_parameters={
433
+ "team_name": self.team_name,
434
+ "force": force,
435
+ },
436
+ )
437
+
438
+ async def disable(self):
439
+ """
440
+ Disable the AI agent team
441
+ """
442
+ async with async_cursor() as cr:
443
+ await cr.callproc(
444
+ "DBMS_CLOUD_AI_AGENT.DISABLE_TEAM",
445
+ keyword_parameters={
446
+ "team_name": self.team_name,
447
+ },
448
+ )
449
+
450
+ async def enable(self):
451
+ """
452
+ Enable the AI agent team
453
+ """
454
+ async with async_cursor() as cr:
455
+ await cr.callproc(
456
+ "DBMS_CLOUD_AI_AGENT.ENABLE_TEAM",
457
+ keyword_parameters={
458
+ "team_name": self.team_name,
459
+ },
460
+ )
461
+
462
+ @classmethod
463
+ async def fetch(cls, team_name: str) -> "AsyncTeam":
464
+ """
465
+ Fetch AI Team attributes from the Database and build a proxy object in
466
+ the Python layer
467
+
468
+ :param str team_name: The name of the AI Team
469
+
470
+ :return: select_ai.agent.Team
471
+
472
+ :raises select_ai.errors.AgentTeamNotFoundError:
473
+ If the AI Team is not found
474
+ """
475
+ attributes = await cls._get_attributes(team_name)
476
+ description = await cls._get_description(team_name)
477
+ return cls(
478
+ team_name=team_name,
479
+ attributes=attributes,
480
+ description=description,
481
+ )
482
+
483
+ @classmethod
484
+ async def list(
485
+ cls, team_name_pattern: Optional[str] = ".*"
486
+ ) -> AsyncGenerator["AsyncTeam", None]:
487
+ """
488
+ List AI Agent Teams
489
+
490
+ :param str team_name_pattern: Regular expressions can be used
491
+ to specify a pattern. Function REGEXP_LIKE is used to perform the
492
+ match. Default value is ".*" i.e. match all teams.
493
+
494
+ :return: Iterator[Team]
495
+
496
+ """
497
+ async with async_cursor() as cr:
498
+ await cr.execute(
499
+ LIST_USER_AI_AGENT_TEAMS,
500
+ team_name_pattern=team_name_pattern,
501
+ )
502
+ rows = await cr.fetchall()
503
+ for row in rows:
504
+ team_name = row[0]
505
+ if row[1]:
506
+ description = await row[1].read() # Oracle.AsyncLOB
507
+ else:
508
+ description = None
509
+ attributes = await cls._get_attributes(team_name=team_name)
510
+ yield cls(
511
+ team_name=team_name,
512
+ description=description,
513
+ attributes=attributes,
514
+ )
515
+
516
+ async def run(self, prompt: str = None, params: Mapping = None):
517
+ """
518
+ Start a new AI agent team or resume a paused one that is waiting
519
+ for human input. If you provide an existing process ID and the
520
+ associated team process is in the WAITING_FOR_HUMAN state, the
521
+ function resumes the workflow using the input you provide as
522
+ the human response
523
+
524
+ :param str prompt: Optional prompt for the user. If the task is
525
+ in the RUNNING state, the input acts as a placeholder for the
526
+ {query} in the task instruction. If the task is in the
527
+ WAITING_FOR_HUMAN state, the input serves as the human response.
528
+
529
+ :param Mapping[str, str] params: Optional parameters for the task.
530
+ Currently, the following parameters are supported:
531
+
532
+ - conversation_id: Identifies the conversation session associated
533
+ with the agent team
534
+
535
+ - variables: key-value pairs that provide additional input to the agent team.
536
+
537
+ """
538
+ parameters = {
539
+ "team_name": self.team_name,
540
+ }
541
+ if prompt:
542
+ parameters["user_prompt"] = prompt
543
+ if params:
544
+ parameters["params"] = json.dumps(params)
545
+
546
+ async with async_cursor() as cr:
547
+ data = await cr.callfunc(
548
+ "DBMS_CLOUD_AI_AGENT.RUN_TEAM",
549
+ oracledb.DB_TYPE_CLOB,
550
+ keyword_parameters=parameters,
551
+ )
552
+ if data is not None:
553
+ result = await data.read()
554
+ else:
555
+ result = None
556
+ return result
557
+
558
+ async def set_attributes(self, attributes: TeamAttributes) -> None:
559
+ """
560
+ Set the attributes of the AI Agent team
561
+ """
562
+ parameters = {
563
+ "object_name": self.team_name,
564
+ "object_type": "team",
565
+ "attributes": attributes.json(),
566
+ }
567
+ async with async_cursor() as cr:
568
+ await cr.callproc(
569
+ "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES",
570
+ keyword_parameters=parameters,
571
+ )
572
+
573
+ async def set_attribute(
574
+ self, attribute_name: str, attribute_value: Any
575
+ ) -> None:
576
+ """
577
+ Set the attribute of the AI Agent team specified by
578
+ `attribute_name` and `attribute_value`.
579
+ """
580
+ parameters = {
581
+ "object_name": self.team_name,
582
+ "object_type": "team",
583
+ "attribute_name": attribute_name,
584
+ "attribute_value": attribute_value,
585
+ }
586
+ async with async_cursor() as cr:
587
+ await cr.callproc(
588
+ "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE",
589
+ keyword_parameters=parameters,
590
+ )