diff --git a/samples/agent/tool_delete.py b/samples/agent/tool_delete.py index c3cfa67..38e0c00 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) +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 7d5b216..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,11 +173,23 @@ 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 """ - with cursor as cr: + with cursor() as cr: cr.callproc( "DBMS_CLOUD_AI_AGENT.DISABLE_AGENT", keyword_parameters={ @@ -189,7 +201,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={ @@ -390,11 +402,25 @@ 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 """ - 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 +432,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/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 cd85071..0918500 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, @@ -585,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 @@ -835,7 +856,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 +989,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, @@ -1019,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 0682e24..c980a96 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, ) @@ -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 @@ -423,9 +440,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..558406f 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, @@ -266,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 @@ -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..ec777b9 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, ) @@ -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 @@ -399,9 +414,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 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/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" 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")