From 57aa6a042db8d2b7decc66ea4a2499e8e1e86f90 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 15 Jan 2026 15:07:45 -0800 Subject: [PATCH 1/5] Bug fixes - CREATE_EMAIL_NOTIFICATION_TOOL AND CREATE_SLACK_TOOL API THROWS ERROR ORA-20052: INVALID VALUE FOR TOOL ATTRIBUTE - TOOL_TYPE - AGENT.DISABLE() AND AGENT.ENABLE() RAISE TYPEERROR DUE TO CURSOR CONTEXT MANAGER --- src/select_ai/agent/core.py | 8 ++++---- src/select_ai/agent/tool.py | 30 ++++++++++++++++++++---------- src/select_ai/version.py | 2 +- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/select_ai/agent/core.py b/src/select_ai/agent/core.py index 7d5b216..4ea8fb1 100644 --- a/src/select_ai/agent/core.py +++ b/src/select_ai/agent/core.py @@ -177,7 +177,7 @@ def disable(self): """ Disable AI Agent """ - with cursor as cr: + with cursor() as cr: cr.callproc( "DBMS_CLOUD_AI_AGENT.DISABLE_AGENT", keyword_parameters={ @@ -189,7 +189,7 @@ def enable(self): """ Enable AI Agent """ - with cursor as cr: + with cursor() as cr: cr.callproc( "DBMS_CLOUD_AI_AGENT.ENABLE_AGENT", keyword_parameters={ @@ -394,7 +394,7 @@ async def disable(self): """ Disable AI Agent """ - async with async_cursor as cr: + async with async_cursor() as cr: await cr.callproc( "DBMS_CLOUD_AI_AGENT.DISABLE_AGENT", keyword_parameters={ @@ -406,7 +406,7 @@ async def enable(self): """ Enable AI Agent """ - async with async_cursor as cr: + async with async_cursor() as cr: await cr.callproc( "DBMS_CLOUD_AI_AGENT.ENABLE_AGENT", keyword_parameters={ diff --git a/src/select_ai/agent/tool.py b/src/select_ai/agent/tool.py index cd85071..1eec9eb 100644 --- a/src/select_ai/agent/tool.py +++ b/src/select_ai/agent/tool.py @@ -48,13 +48,12 @@ class ToolType(StrEnum): Built-in Tool Types """ - EMAIL = "EMAIL" HUMAN = "HUMAN" HTTP = "HTTP" RAG = "RAG" SQL = "SQL" - SLACK = "SLACK" WEBSEARCH = "WEBSEARCH" + NOTIFICATION = "NOTIFICATION" @dataclass @@ -103,6 +102,12 @@ def __post_init__(self): @classmethod def create(cls, *, tool_type: Optional[ToolType] = None, **kwargs): tool_params_cls = ToolTypeParams.get(tool_type, ToolParams) + if "notification_type" in kwargs: + notification_type = kwargs["notification_type"] + if notification_type == NotificationType.SLACK: + tool_params_cls = SlackNotificationToolParams + elif notification_type == NotificationType.EMAIL: + tool_params_cls = EmailNotificationToolParams return tool_params_cls(**kwargs) @classmethod @@ -132,14 +137,20 @@ class RAGToolParams(ToolParams): @dataclass -class SlackNotificationToolParams(ToolParams): +class NotificationToolParams(ToolParams): + + notification_type = NotificationType + + +@dataclass +class SlackNotificationToolParams(NotificationToolParams): _REQUIRED_FIELDS = ["credential_name", "slack_channel"] notification_type: NotificationType = NotificationType.SLACK @dataclass -class EmailNotificationToolParams(ToolParams): +class EmailNotificationToolParams(NotificationToolParams): _REQUIRED_FIELDS = ["credential_name", "recipient", "sender", "smtp_host"] notification_type: NotificationType = NotificationType.EMAIL @@ -222,8 +233,7 @@ def create(cls, **kwargs): ToolTypeParams = { - ToolType.EMAIL: EmailNotificationToolParams, - ToolType.SLACK: SlackNotificationToolParams, + ToolType.NOTIFICATION: NotificationToolParams, ToolType.HTTP: HTTPToolParams, ToolType.RAG: RAGToolParams, ToolType.SQL: SQLToolParams, @@ -401,7 +411,7 @@ def create_email_notification_tool( ) return cls.create_built_in_tool( tool_name=tool_name, - tool_type=ToolType.EMAIL, + tool_type=ToolType.NOTIFICATION, tool_params=email_notification_tool_params, description=description, replace=replace, @@ -534,7 +544,7 @@ def create_slack_notification_tool( ) return cls.create_built_in_tool( tool_name=tool_name, - tool_type=ToolType.SLACK, + tool_type=ToolType.NOTIFICATION, tool_params=slack_notification_tool_params, description=description, replace=replace, @@ -835,7 +845,7 @@ async def create_email_notification_tool( ) return await cls.create_built_in_tool( tool_name=tool_name, - tool_type=ToolType.EMAIL, + tool_type=ToolType.NOTIFICATION, tool_params=email_notification_tool_params, description=description, replace=replace, @@ -968,7 +978,7 @@ async def create_slack_notification_tool( ) return await cls.create_built_in_tool( tool_name=tool_name, - tool_type=ToolType.SLACK, + tool_type=ToolType.NOTIFICATION, tool_params=slack_notification_tool_params, description=description, replace=replace, diff --git a/src/select_ai/version.py b/src/select_ai/version.py index 989c84d..8e5d932 100644 --- a/src/select_ai/version.py +++ b/src/select_ai/version.py @@ -5,4 +5,4 @@ # http://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -__version__ = "1.2.1" +__version__ = "1.2.2" From ff7ea4cb7d3c5193a1b4dd064497feb5f3dca1c9 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 15 Jan 2026 16:04:30 -0800 Subject: [PATCH 2/5] Fixed UNCLEAR ERROR FOR RUN_SQL WITH INVALID OR NON-EXISTENT TABLES --- src/select_ai/async_profile.py | 6 ++---- src/select_ai/base_profile.py | 14 +++++++++++++- src/select_ai/errors.py | 10 ++++++++++ src/select_ai/profile.py | 6 ++---- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index 0682e24..7bc9a1d 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -23,7 +23,7 @@ from select_ai.base_profile import ( BaseProfile, ProfileAttributes, - no_data_for_prompt, + convert_json_rows_to_df, validate_params_for_feedback, validate_params_for_summary, ) @@ -423,9 +423,7 @@ async def generate( else: result = None if action == Action.RUNSQL: - if no_data_for_prompt(result): # empty dataframe - return pandas.DataFrame() - return pandas.DataFrame(json.loads(result)) + return convert_json_rows_to_df(result) else: return result diff --git a/src/select_ai/base_profile.py b/src/select_ai/base_profile.py index 0e37b8f..332959a 100644 --- a/src/select_ai/base_profile.py +++ b/src/select_ai/base_profile.py @@ -12,10 +12,11 @@ from typing import List, Mapping, Optional, Tuple import oracledb +import pandas from select_ai._abc import SelectAIDataClass from select_ai.action import Action -from select_ai.errors import ProfileExistsError +from select_ai.errors import InvalidSQLError, ProfileExistsError from select_ai.feedback import ( FeedbackOperation, FeedbackType, @@ -298,3 +299,14 @@ def validate_params_for_summary( if params: parameters["parameters"] = params.json() return parameters + + +def convert_json_rows_to_df(result): + if no_data_for_prompt(result): # empty dataframe + return pandas.DataFrame() + try: + rows = json.loads(result) + except json.decoder.JSONDecodeError: + raise InvalidSQLError(result) + else: + return pandas.DataFrame(rows) diff --git a/src/select_ai/errors.py b/src/select_ai/errors.py index 3da4042..718c22d 100644 --- a/src/select_ai/errors.py +++ b/src/select_ai/errors.py @@ -123,3 +123,13 @@ def __init__(self, team_name: str): def __str__(self): return f"Agent Team {self.team_name} not found" + + +class InvalidSQLError(SelectAIError): + """Invalid SQL generated""" + + def __init__(self, error_message: str): + self.message = error_message + + def __str__(self): + return self.message diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index 7001349..60f181a 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -17,7 +17,7 @@ from select_ai.base_profile import ( BaseProfile, ProfileAttributes, - no_data_for_prompt, + convert_json_rows_to_df, validate_params_for_feedback, validate_params_for_summary, ) @@ -399,9 +399,7 @@ def generate( else: result = None if action == Action.RUNSQL: - if no_data_for_prompt(result): # empty dataframe - return pandas.DataFrame() - return pandas.DataFrame(json.loads(result)) + return convert_json_rows_to_df(result) else: return result From 83371697802852fca4b35347c42147a17b9e63cd Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Tue, 20 Jan 2026 22:59:30 -0800 Subject: [PATCH 3/5] Added class level delete object methods --- samples/agent/tool_delete.py | 3 +- samples/profile_delete.py | 3 +- src/select_ai/agent/core.py | 28 +++++++++++++++++- src/select_ai/agent/task.py | 22 ++++++++++++++ src/select_ai/agent/team.py | 26 ++++++++++++++-- src/select_ai/agent/tool.py | 22 ++++++++++++++ src/select_ai/async_profile.py | 33 ++++++++++++++++----- src/select_ai/profile.py | 31 ++++++++++++++----- src/select_ai/vector_index.py | 36 +++++++++++++++++++++++ tests/profiles/test_1200_profile.py | 4 ++- tests/profiles/test_1300_profile_async.py | 4 ++- 11 files changed, 187 insertions(+), 25 deletions(-) diff --git a/samples/agent/tool_delete.py b/samples/agent/tool_delete.py index c3cfa67..4f400bc 100644 --- a/samples/agent/tool_delete.py +++ b/samples/agent/tool_delete.py @@ -21,5 +21,4 @@ select_ai.connect(user=user, password=password, dsn=dsn) -tool = select_ai.agent.Tool("LLM_CHAT_TOOL") -tool.delete(force=True) +tool = select_ai.agent.Tool.delete_tool("LLM_CHAT_TOOL", force=True) diff --git a/samples/profile_delete.py b/samples/profile_delete.py index 3cee3b4..475e9cd 100644 --- a/samples/profile_delete.py +++ b/samples/profile_delete.py @@ -18,5 +18,4 @@ dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) -profile = select_ai.Profile(profile_name="oci_ai_profile") -profile.delete() +profile = select_ai.Profile.delete_profile(profile_name="oci_ai_profile") diff --git a/src/select_ai/agent/core.py b/src/select_ai/agent/core.py index 4ea8fb1..02590eb 100644 --- a/src/select_ai/agent/core.py +++ b/src/select_ai/agent/core.py @@ -1,4 +1,4 @@ -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- # Copyright (c) 2025, Oracle and/or its affiliates. # # Licensed under the Universal Permissive License v 1.0 as shown at @@ -173,6 +173,18 @@ def delete(self, force: Optional[bool] = False): }, ) + @classmethod + def delete_agent(cls, agent_name: str, force: Optional[bool] = False): + """ + Class method to delete AI Agent from the database + + :param str agent_name: The name of the AI Agent + :param bool force: Force the deletion. Default value is False. + + """ + agent = cls(agent_name=agent_name) + agent.delete(force=force) + def disable(self): """ Disable AI Agent @@ -390,6 +402,20 @@ async def delete(self, force: Optional[bool] = False): }, ) + @classmethod + async def delete_agent( + cls, agent_name: str, force: Optional[bool] = False + ): + """ + Class method to delete AI Agent from the database + + :param str agent_name: The name of the AI Agent + :param bool force: Force the deletion. Default value is False. + + """ + agent = cls(agent_name=agent_name) + await agent.delete(force=force) + async def disable(self): """ Disable AI Agent diff --git a/src/select_ai/agent/task.py b/src/select_ai/agent/task.py index 2fe8262..d0fe70d 100644 --- a/src/select_ai/agent/task.py +++ b/src/select_ai/agent/task.py @@ -188,6 +188,17 @@ def delete(self, force: bool = False): }, ) + @classmethod + def delete_task(cls, task_name: str, force: bool = False): + """ + Class method to delete AI Task from the database + + :param str task_name: The name of the AI Task + :param bool force: Force the deletion. Default value is False. + """ + task = cls(task_name=task_name) + task.delete(force=force) + def disable(self): """ Disable AI Task @@ -406,6 +417,17 @@ async def delete(self, force: bool = False): }, ) + @classmethod + async def delete_task(cls, task_name: str, force: bool = False): + """ + Class method to delete AI Task from the database + + :param str task_name: The name of the AI Task + :param bool force: Force the deletion. Default value is False. + """ + task = cls(task_name=task_name) + await task.delete(force=force) + async def disable(self): """ Disable AI Task diff --git a/src/select_ai/agent/team.py b/src/select_ai/agent/team.py index a128a35..1392218 100644 --- a/src/select_ai/agent/team.py +++ b/src/select_ai/agent/team.py @@ -57,10 +57,10 @@ class BaseTeam(ABC): def __init__( self, team_name: str, - attributes: TeamAttributes, + attributes: Optional[TeamAttributes] = None, description: Optional[str] = None, ): - if not isinstance(attributes, TeamAttributes): + if attributes and not isinstance(attributes, TeamAttributes): raise TypeError( f"attributes must be an object of type " f"select_ai.agent.TeamAttributes instance" @@ -180,6 +180,17 @@ def delete(self, force: Optional[bool] = False): }, ) + @classmethod + def delete_team(cls, team_name: str, force: Optional[bool] = False): + """ + Class method to delete an AI agent team from the database + + :param str team_name: The name of the AI team + :param bool force: Force the deletion. Default value is False. + """ + team = cls(team_name=team_name) + team.delete(force=force) + def disable(self): """ Disable the AI agent team @@ -435,6 +446,17 @@ async def delete(self, force: Optional[bool] = False): }, ) + @classmethod + async def delete_team(cls, team_name: str, force: Optional[bool] = False): + """ + Class method to delete an AI agent team from the database + + :param str team_name: The name of the AI team + :param bool force: Force the deletion. Default value is False. + """ + team = cls(team_name=team_name) + await team.delete(force=force) + async def disable(self): """ Disable the AI agent team diff --git a/src/select_ai/agent/tool.py b/src/select_ai/agent/tool.py index 1eec9eb..0918500 100644 --- a/src/select_ai/agent/tool.py +++ b/src/select_ai/agent/tool.py @@ -595,6 +595,17 @@ def delete(self, force: bool = False): }, ) + @classmethod + def delete_tool(cls, tool_name: str, force: bool = False): + """ + Class method to delete AI Tool from the database + + :param str tool_name: The name of the tool + :param bool force: Force the deletion. Default value is False. + """ + tool = cls(tool_name=tool_name) + tool.delete(force=force) + def disable(self): """ Disable AI Tool @@ -1029,6 +1040,17 @@ async def delete(self, force: bool = False): }, ) + @classmethod + async def delete_tool(cls, tool_name: str, force: bool = False): + """ + Class method ot delete AI Tool from the database + + :param str tool_name: The name of the tool + :param bool force: Force the deletion. Default value is False. + """ + tool = cls(tool_name=tool_name) + await tool.delete(force=force) + async def disable(self): """ Disable AI Tool diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index 7bc9a1d..c980a96 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -248,23 +248,40 @@ async def create(self, replace: Optional[int] = False) -> None: else: raise - async def delete(self, force=False) -> None: - """Asynchronously deletes an AI profile from the database - - :param bool force: Ignores errors if AI profile does not exist. - :return: None - :raises: oracledb.DatabaseError - + @staticmethod + async def _delete(profile_name: str, force: bool = False): + """ + Internal method to delete AI profile from the database """ async with async_cursor() as cr: await cr.callproc( "DBMS_CLOUD_AI.DROP_PROFILE", keyword_parameters={ - "profile_name": self.profile_name, + "profile_name": profile_name, "force": force, }, ) + async def delete(self, force=False) -> None: + """Asynchronously deletes an AI profile from the database + + :param bool force: Ignores errors if AI profile does not exist. + :return: None + :raises: oracledb.DatabaseError + """ + await self._delete(profile_name=self.profile_name, force=force) + + @classmethod + async def delete_profile(cls, profile_name: str, force: bool = False): + """Asynchronously deletes an AI profile from the database + + :param str profile_name: Name of the AI profile + :param bool force: Ignores errors if AI profile does not exist. + :return: None + :raises: oracledb.DatabaseError + """ + await cls._delete(profile_name=profile_name, force=force) + @classmethod async def fetch(cls, profile_name: str) -> "AsyncProfile": """Asynchronously create an AI Profile object from attributes diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index 60f181a..ec777b9 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -227,22 +227,37 @@ def create(self, replace: Optional[int] = False) -> None: else: raise - def delete(self, force=False) -> None: - """Deletes an AI profile from the database - - :param bool force: Ignores errors if AI profile does not exist. - :return: None - :raises: oracledb.DatabaseError - """ + @staticmethod + def _delete(profile_name: str, force: bool = False): with cursor() as cr: cr.callproc( "DBMS_CLOUD_AI.DROP_PROFILE", keyword_parameters={ - "profile_name": self.profile_name, + "profile_name": profile_name, "force": force, }, ) + def delete(self, force=False) -> None: + """Deletes an AI profile from the database + + :param bool force: Ignores errors if AI profile does not exist. + :return: None + :raises: oracledb.DatabaseError + """ + self._delete(profile_name=self.profile_name, force=force) + + @classmethod + def delete_profile(cls, profile_name: str, force: bool = False): + """Class method to delete an AI profile from the database + + :param str profile_name: Name of the AI profile + :param bool force: Ignores errors if AI profile does not exist. + :return: None + :raises: oracledb.DatabaseError + """ + cls._delete(profile_name=profile_name, force=force) + @classmethod def fetch(cls, profile_name: str) -> "Profile": """Create a proxy Profile object from fetched attributes saved in the diff --git a/src/select_ai/vector_index.py b/src/select_ai/vector_index.py index 53e8158..dd5166a 100644 --- a/src/select_ai/vector_index.py +++ b/src/select_ai/vector_index.py @@ -231,6 +231,24 @@ def create(self, replace: Optional[bool] = False): raise self.profile.set_attribute("vector_index_name", self.index_name) + @classmethod + def delete_index( + cls, index_name: str, include_data: bool = True, force: bool = False + ): + """Class method to remove a vector store index + + :param str index_name: The name of the vector index + :param bool include_data: Indicates whether to delete + both the customer's vector store and vector index + along with the vector index object + :param bool force: Indicates whether to ignore errors + that occur if the vector index does not exist + :return: None + :raises: oracledb.DatabaseError + """ + index = cls(index_name=index_name) + index.delete(force=force, include_data=include_data) + def delete( self, include_data: Optional[bool] = True, @@ -537,6 +555,24 @@ async def delete( }, ) + @classmethod + async def delete_index( + cls, index_name: str, include_data: bool = True, force: bool = False + ): + """Class method to remove a vector store index + + :param str index_name: The name of the vector index + :param bool include_data: Indicates whether to delete + both the customer's vector store and vector index + along with the vector index object + :param bool force: Indicates whether to ignore errors + that occur if the vector index does not exist + :return: None + :raises: oracledb.DatabaseError + """ + index = cls(index_name=index_name) + await index.delete(force=force, include_data=include_data) + async def enable(self) -> None: """This procedure enables or activates a previously disabled vector index object. Generally, when you create a vector index, by default diff --git a/tests/profiles/test_1200_profile.py b/tests/profiles/test_1200_profile.py index e4d327e..030d006 100644 --- a/tests/profiles/test_1200_profile.py +++ b/tests/profiles/test_1200_profile.py @@ -35,7 +35,9 @@ def python_gen_ai_profile(profile_attributes): ) yield profile logger.info("Deleting profile %s", profile.profile_name) - profile.delete(force=True) + select_ai.Profile.delete_profile( + profile_name=PYSAI_1200_PROFILE, force=True + ) @pytest.fixture(scope="module") diff --git a/tests/profiles/test_1300_profile_async.py b/tests/profiles/test_1300_profile_async.py index 7626f74..3f0d8c6 100644 --- a/tests/profiles/test_1300_profile_async.py +++ b/tests/profiles/test_1300_profile_async.py @@ -39,7 +39,9 @@ async def python_gen_ai_profile(profile_attributes): logger.debug("AsyncProfile = \n %s", profile) yield profile logger.info("Deleting async profile %s", profile.profile_name) - await profile.delete(force=True) + await AsyncProfile.delete_profile( + profile_name=PYSAI_ASYNC_1300_PROFILE, force=True + ) @pytest.fixture(scope="module") From 97671ab33ed98b308e461a42658e946409f2af0c Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 21 Jan 2026 19:14:12 -0800 Subject: [PATCH 4/5] Behaviour in-sync with PL/SQL API --- src/select_ai/base_profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/select_ai/base_profile.py b/src/select_ai/base_profile.py index 332959a..558406f 100644 --- a/src/select_ai/base_profile.py +++ b/src/select_ai/base_profile.py @@ -267,7 +267,7 @@ def validate_params_for_feedback( ) sql_text = "select ai {} {}".format(action, prompt) parameters["sql_text"] = sql_text - elif sql_id: + if sql_id: parameters["sql_id"] = sql_id return parameters From 86a8bcef2269cafe8929d1d45eb7eba979abd43c Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 21 Jan 2026 19:16:02 -0800 Subject: [PATCH 5/5] Updated sample to delete Tool --- samples/agent/tool_delete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/agent/tool_delete.py b/samples/agent/tool_delete.py index 4f400bc..38e0c00 100644 --- a/samples/agent/tool_delete.py +++ b/samples/agent/tool_delete.py @@ -21,4 +21,4 @@ select_ai.connect(user=user, password=password, dsn=dsn) -tool = select_ai.agent.Tool.delete_tool("LLM_CHAT_TOOL", force=True) +select_ai.agent.Tool.delete_tool("LLM_CHAT_TOOL", force=True)