From 20b57832cdec4163a9b8eed5db48d78e23a27deb Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Thu, 16 May 2024 17:48:11 -0300 Subject: [PATCH 1/8] Agent CRUD --- .pre-commit-config.yaml | 9 +- aixplain/factories/__init__.py | 1 + aixplain/factories/agent_factory/__init__.py | 118 +++++++++++++++++++ aixplain/factories/agent_factory/utils.py | 29 +++++ aixplain/modules/__init__.py | 2 + aixplain/modules/agent/__init__.py | 75 ++++++++++++ aixplain/modules/agent/tool.py | 59 ++++++++++ aixplain/modules/model.py | 11 +- 8 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 aixplain/factories/agent_factory/__init__.py create mode 100644 aixplain/factories/agent_factory/utils.py create mode 100644 aixplain/modules/agent/__init__.py create mode 100644 aixplain/modules/agent/tool.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1395dfa6..a79973ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,4 +15,11 @@ repos: - id: black language_version: python3 args: # arguments to configure black - - --line-length=128 \ No newline at end of file + - --line-length=128 + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.0.0 # Use the latest version + hooks: + - id: flake8 + args: # arguments to configure black + - --ignore=E402,E501 \ No newline at end of file diff --git a/aixplain/factories/__init__.py b/aixplain/factories/__init__.py index 36147c6e..7b876899 100644 --- a/aixplain/factories/__init__.py +++ b/aixplain/factories/__init__.py @@ -20,6 +20,7 @@ limitations under the License. """ from .asset_factory import AssetFactory +from .agent_factory import AgentFactory from .benchmark_factory import BenchmarkFactory from .corpus_factory import CorpusFactory from .data_factory import DataFactory diff --git a/aixplain/factories/agent_factory/__init__.py b/aixplain/factories/agent_factory/__init__.py new file mode 100644 index 00000000..51297763 --- /dev/null +++ b/aixplain/factories/agent_factory/__init__.py @@ -0,0 +1,118 @@ +__author__ = "lucaspavanelli" + +""" +Copyright 2024 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Thiago Castro Ferreira and Lucas Pavanelli +Date: May 16th 2024 +Description: + Agent Factory Class +""" + +import json +import logging + +from aixplain.enums.supplier import Supplier +from aixplain.modules.agent import Agent, Tool +from aixplain.utils import config +from typing import Dict, List, Optional, Text, Union + +from aixplain.factories.agent_factory.utils import build_agent +from aixplain.utils.file_utils import _request_with_retry + + +class AgentFactory: + @classmethod + def create( + cls, + name: Text, + tools: List[Tool] = [], + description: Text = "", + api_key: Optional[Text] = config.TEAM_API_KEY, + supplier: Union[Dict, Text, Supplier, int] = "aiXplain", + version: Optional[Text] = None, + cost: Optional[Dict] = None, + ) -> Agent: + """Create a new agent in the platform.""" + try: + agent = None + url = "http://54.86.247.242:8000/execute" + headers = {"Authorization": "token " + api_key} + + if isinstance(Supplier, dict): + supplier = supplier["code"] + elif isinstance(supplier, Supplier): + supplier = supplier.value + + payload = { + "name": name, + "tools": [tool.function.value for tool in tools], + "description": description, + "supplier": supplier, + "version": version, + "cost": cost, + } + logging.info(f"Start service for POST Create Agent - {url} - {headers}") + r = _request_with_retry("post", url, headers=headers, json=payload) + if 200 <= r.status_code < 300: + response = r.json() + + asset_id = response["id"] + agent = Agent( + id=asset_id, name=name, tools=tools, description=description, supplier=supplier, version=version, cost=cost + ) + else: + error_msg = "Agent Onboarding Error: Please contant the administrators." + logging.exception(error_msg) + raise Exception(error_msg) + except Exception as e: + raise Exception(e) + return agent + + @classmethod + def list(cls) -> Dict: + """List all agents available in the platform.""" + url = "http://54.86.247.242:8000/list" + if config.AIXPLAIN_API_KEY != "": + headers = {"x-aixplain-key": f"{config.AIXPLAIN_API_KEY}", "Content-Type": "application/json"} + else: + headers = {"Authorization": f"Token {config.TEAM_API_KEY}", "Content-Type": "application/json"} + + payload = {} + logging.info(f"Start service for POST List Agents - {url} - {headers} - {json.dumps(payload)}") + r = _request_with_retry("get", url, headers=headers) + resp = r.json() + + agents, page_total, total = [], 0, 0 + results = resp + page_total = len(results) + total = len(results) + logging.info(f"Response for POST List Dataset - Page Total: {page_total} / Total: {total}") + for agent in results: + agents.append(build_agent(agent)) + return {"results": agents, "page_total": page_total, "page_number": 0, "total": total} + + @classmethod + def get(cls, agent_id: Text) -> Agent: + """Get agent by id.""" + url = f"http://54.86.247.242:8000/get?id={agent_id}" + if config.AIXPLAIN_API_KEY != "": + headers = {"x-aixplain-key": f"{config.AIXPLAIN_API_KEY}", "Content-Type": "application/json"} + else: + headers = {"Authorization": f"Token {config.TEAM_API_KEY}", "Content-Type": "application/json"} + logging.info(f"Start service for GET Agent - {url} - {headers}") + r = _request_with_retry("get", url, headers=headers) + resp = r.json() + return build_agent(resp) diff --git a/aixplain/factories/agent_factory/utils.py b/aixplain/factories/agent_factory/utils.py new file mode 100644 index 00000000..952c7f69 --- /dev/null +++ b/aixplain/factories/agent_factory/utils.py @@ -0,0 +1,29 @@ +__author__ = "thiagocastroferreira" + +from aixplain.enums.function import Function +from aixplain.modules.agent import Agent, Tool +from typing import Dict + + +def build_agent(payload: Dict) -> Agent: + """Instantiate a new agent in the platform.""" + tools = payload["tools"] + for i, tool in enumerate(tools): + try: + function = Function(tool) + except Exception: + function = tool + + tools[i] = Tool(name=tool, description=tool, function=function, supplier=None) + + agent = Agent( + id=payload["id"], + name=payload["name"] if "name" in payload else "", + tools=tools, + description=payload["description"] if "description" in payload else "", + supplier=payload["supplier"] if "supplier" in payload else None, + version=payload["version"] if "version" in payload else None, + cost=payload["cost"] if "cost" in payload else None, + ) + agent.url = "http://54.86.247.242:8000/execute" + return agent diff --git a/aixplain/modules/__init__.py b/aixplain/modules/__init__.py index bb9e696b..5241f3c9 100644 --- a/aixplain/modules/__init__.py +++ b/aixplain/modules/__init__.py @@ -32,3 +32,5 @@ from .finetune.status import FinetuneStatus from .benchmark import Benchmark from .benchmark_job import BenchmarkJob +from .agent import Agent +from .agent.tool import Tool diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py new file mode 100644 index 00000000..1078b57f --- /dev/null +++ b/aixplain/modules/agent/__init__.py @@ -0,0 +1,75 @@ +__author__ = "aiXplain" + +""" +Copyright 2024 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Lucas Pavanelli and Thiago Castro Ferreira +Date: May 16th 2024 +Description: + Agentification Class +""" +from aixplain.enums.supplier import Supplier +from aixplain.modules.model import Model +from aixplain.modules.agent.tool import Tool +from typing import Dict, List, Text, Optional, Union + +from aixplain.utils import config + + +class Agent(Model): + """Advanced AI system capable of performing tasks by leveraging specialized software tools and resources from aiXplain marketplace. + + Attributes: + id (Text): ID of the Agent + name (Text): Name of the Agent + tools (List[Tool]): List of tools that the Agent uses. + description (Text, optional): description of the Agent. Defaults to "". + supplier (Text): Supplier of the Agent. + version (Text): Version of the Agent. + backend_url (str): URL of the backend. + api_key (str): The TEAM API key used for authentication. + cost (Dict, optional): model price. Defaults to None. + """ + + def __init__( + self, + id: Text, + name: Text, + tools: List[Tool] = [], + description: Text = "", + api_key: Optional[Text] = config.TEAM_API_KEY, + supplier: Union[Dict, Text, Supplier, int] = "aiXplain", + version: Optional[Text] = None, + cost: Optional[Dict] = None, + **additional_info, + ) -> None: + """Create a FineTune with the necessary information. + + Args: + id (Text): ID of the Agent + name (Text): Name of the Agent + tools (List[Tool]): List of tools that the Agent uses. + description (Text, optional): description of the Agent. Defaults to "". + supplier (Text): Supplier of the Agent. + version (Text): Version of the Agent. + backend_url (str): URL of the backend. + api_key (str): The TEAM API key used for authentication. + cost (Dict, optional): model price. Defaults to None. + **additional_info: Additional information to be saved with the FineTune. + """ + assert len(tools) > 0, "At least one tool must be provided." + super().__init__(id, name, description, api_key, supplier, version, cost=cost) + self.additional_info = additional_info + self.tools = tools diff --git a/aixplain/modules/agent/tool.py b/aixplain/modules/agent/tool.py new file mode 100644 index 00000000..04334992 --- /dev/null +++ b/aixplain/modules/agent/tool.py @@ -0,0 +1,59 @@ +__author__ = "aiXplain" + +""" +Copyright 2024 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Lucas Pavanelli and Thiago Castro Ferreira +Date: May 16th 2024 +Description: + Agentification Class +""" +from typing import Dict, Text, Optional, Union + +from aixplain.enums.function import Function +from aixplain.enums.supplier import Supplier + + +class Tool: + """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. + + Attributes: + name (Text): name of the tool + description (Text): descriptiion of the tool + function (Function): task that the tool performs + supplier (Optional[Union[Dict, Text, Supplier, int]], optional): Preferred supplier to perform the task. Defaults to None. + """ + + def __init__( + self, + name: Text, + description: Text, + function: Function, + supplier: Optional[Union[Dict, Text, Supplier, int]] = None, + **additional_info, + ) -> None: + """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. + + Args: + name (Text): name of the tool + description (Text): descriptiion of the tool + function (Function): task that the tool performs + supplier (Optional[Union[Dict, Text, Supplier, int]], optional): Preferred supplier to perform the task. Defaults to None. + """ + self.name = name + self.description = description + self.function = function + self.supplier = supplier + self.additional_info = additional_info diff --git a/aixplain/modules/model.py b/aixplain/modules/model.py index 983737c7..7679e798 100644 --- a/aixplain/modules/model.py +++ b/aixplain/modules/model.py @@ -24,9 +24,8 @@ import json import logging import traceback -from typing import List from aixplain.factories.file_factory import FileFactory -from aixplain.enums import Function, Supplier +from aixplain.enums import Supplier from aixplain.modules.asset import Asset from aixplain.utils import config from urllib.parse import urljoin @@ -57,7 +56,7 @@ def __init__( id: Text, name: Text, description: Text = "", - api_key: Optional[Text] = None, + api_key: Text = config.TEAM_API_KEY, supplier: Union[Dict, Text, Supplier, int] = "aiXplain", version: Optional[Text] = None, function: Optional[Text] = None, @@ -229,7 +228,7 @@ def run_async(self, data: Union[Text, Dict], name: Text = "model_process", param if isinstance(payload, int) is True or isinstance(payload, float) is True: payload = str(payload) payload = {"data": payload} - except Exception as e: + except Exception: payload = {"data": data} payload.update(parameters) payload = json.dumps(payload) @@ -245,7 +244,7 @@ def run_async(self, data: Union[Text, Dict], name: Text = "model_process", param poll_url = resp["data"] response = {"status": "IN_PROGRESS", "url": poll_url} - except Exception as e: + except Exception: response = {"status": "FAILED"} msg = f"Error in request for {name} - {traceback.format_exc()}" logging.error(f"Model Run Async: Error in running for {name}: {resp}") @@ -314,7 +313,7 @@ def check_finetune_status(self, after_epoch: Optional[int] = None): logging.info(f"Response for GET Check FineTune status Model - Id {self.id} / Status {status.status.value}.") return status - except Exception as e: + except Exception: message = "" if resp is not None and "statusCode" in resp: status_code = resp["statusCode"] From 46f757dc588d58f3040c0bfc32e99cda914636b3 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Fri, 17 May 2024 02:02:51 -0300 Subject: [PATCH 2/8] Fixes in the structure --- aixplain/factories/agent_factory/__init__.py | 24 +++++++++++++++----- aixplain/factories/agent_factory/utils.py | 12 ++++++---- aixplain/modules/agent/__init__.py | 2 ++ aixplain/modules/agent/tool.py | 4 ++-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/aixplain/factories/agent_factory/__init__.py b/aixplain/factories/agent_factory/__init__.py index 51297763..0be2c145 100644 --- a/aixplain/factories/agent_factory/__init__.py +++ b/aixplain/factories/agent_factory/__init__.py @@ -44,28 +44,40 @@ def create( supplier: Union[Dict, Text, Supplier, int] = "aiXplain", version: Optional[Text] = None, cost: Optional[Dict] = None, + llm_id: Optional[Text] = None, ) -> Agent: """Create a new agent in the platform.""" try: agent = None - url = "http://54.86.247.242:8000/execute" + url = "http://54.86.247.242:8000/create" headers = {"Authorization": "token " + api_key} - if isinstance(Supplier, dict): + if isinstance(supplier, dict): supplier = supplier["code"] elif isinstance(supplier, Supplier): - supplier = supplier.value + supplier = supplier.value["code"] payload = { "name": name, - "tools": [tool.function.value for tool in tools], + "tools": [ + { + "function": tool.function.value, + "name": tool.name, + "description": tool.description, + "supplier": tool.supplier.value if tool.supplier else None, + } + for tool in tools + ], "description": description, "supplier": supplier, "version": version, "cost": cost, } - logging.info(f"Start service for POST Create Agent - {url} - {headers}") - r = _request_with_retry("post", url, headers=headers, json=payload) + + if llm_id is not None: + payload["language_model_id"] = llm_id + logging.info(f"Start service for POST Create Agent - {url} - {headers} - {json.dumps(payload)}") + r = _request_with_retry("post", url, headers=headers, data=json.dumps(payload)) if 200 <= r.status_code < 300: response = r.json() diff --git a/aixplain/factories/agent_factory/utils.py b/aixplain/factories/agent_factory/utils.py index 952c7f69..f3fbdc1b 100644 --- a/aixplain/factories/agent_factory/utils.py +++ b/aixplain/factories/agent_factory/utils.py @@ -1,6 +1,7 @@ __author__ = "thiagocastroferreira" from aixplain.enums.function import Function +from aixplain.enums.supplier import Supplier from aixplain.modules.agent import Agent, Tool from typing import Dict @@ -9,12 +10,14 @@ def build_agent(payload: Dict) -> Agent: """Instantiate a new agent in the platform.""" tools = payload["tools"] for i, tool in enumerate(tools): + function = Function(tool["function"]) + try: - function = Function(tool) + supplier = Supplier(tool["supplier"]) except Exception: - function = tool + supplier = None - tools[i] = Tool(name=tool, description=tool, function=function, supplier=None) + tools[i] = Tool(name=tool["name"], description=tool["description"], function=function, supplier=supplier) agent = Agent( id=payload["id"], @@ -24,6 +27,7 @@ def build_agent(payload: Dict) -> Agent: supplier=payload["supplier"] if "supplier" in payload else None, version=payload["version"] if "version" in payload else None, cost=payload["cost"] if "cost" in payload else None, + llm_id=payload["language_model_id"] if "language_model_id" in payload else None, ) - agent.url = "http://54.86.247.242:8000/execute" + agent.url = "http://54.86.247.242:8000/async-execute" return agent diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index 1078b57f..948ad63f 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -53,6 +53,7 @@ def __init__( supplier: Union[Dict, Text, Supplier, int] = "aiXplain", version: Optional[Text] = None, cost: Optional[Dict] = None, + llm_id: Optional[Text] = None, **additional_info, ) -> None: """Create a FineTune with the necessary information. @@ -73,3 +74,4 @@ def __init__( super().__init__(id, name, description, api_key, supplier, version, cost=cost) self.additional_info = additional_info self.tools = tools + self.llm_id = llm_id diff --git a/aixplain/modules/agent/tool.py b/aixplain/modules/agent/tool.py index 04334992..6651afe7 100644 --- a/aixplain/modules/agent/tool.py +++ b/aixplain/modules/agent/tool.py @@ -20,7 +20,7 @@ Description: Agentification Class """ -from typing import Dict, Text, Optional, Union +from typing import Text, Optional from aixplain.enums.function import Function from aixplain.enums.supplier import Supplier @@ -41,7 +41,7 @@ def __init__( name: Text, description: Text, function: Function, - supplier: Optional[Union[Dict, Text, Supplier, int]] = None, + supplier: Optional[Supplier] = None, **additional_info, ) -> None: """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. From 613691e7b6a06e2eaf8424f1e7a37c40114d77c5 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Fri, 17 May 2024 10:28:58 -0300 Subject: [PATCH 3/8] Delete agent method --- aixplain/modules/agent/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index 948ad63f..11474666 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -20,6 +20,9 @@ Description: Agentification Class """ +import logging + +from aixplain.utils.file_utils import _request_with_retry from aixplain.enums.supplier import Supplier from aixplain.modules.model import Model from aixplain.modules.agent.tool import Tool @@ -75,3 +78,17 @@ def __init__( self.additional_info = additional_info self.tools = tools self.llm_id = llm_id + + def delete(self) -> None: + """Delete Corpus service""" + try: + url = f"http://54.86.247.242:8000/delete/{self.id}" + headers = {"Authorization": f"Token {config.TEAM_API_KEY}", "Content-Type": "application/json"} + logging.info(f"Start service for DELETE Agent - {url} - {headers}") + r = _request_with_retry("delete", url, headers=headers) + if r.status_code != 200: + raise Exception() + except Exception: + message = "Agent Deletion Error: Make sure the agent exists and you are the owner." + logging.error(message) + raise Exception(f"{message}") From 2d7541253990f9dbde1aa951ea575b5c53029e3d Mon Sep 17 00:00:00 2001 From: Lucas Pavanelli <86805709+lucas-aixplain@users.noreply.github.com> Date: Wed, 29 May 2024 12:09:02 -0300 Subject: [PATCH 4/8] Add input/output to PipelineFactory and use api_key from parameter (#182) --- aixplain/factories/pipeline_factory.py | 13 +++++++++++-- pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/aixplain/factories/pipeline_factory.py b/aixplain/factories/pipeline_factory.py index 404a5556..ce552712 100644 --- a/aixplain/factories/pipeline_factory.py +++ b/aixplain/factories/pipeline_factory.py @@ -45,6 +45,11 @@ class PipelineFactory: aixplain_key = config.AIXPLAIN_API_KEY backend_url = config.BACKEND_URL + @classmethod + def __get_typed_nodes(cls, response: Dict, type: str) -> List[Dict]: + # read "nodes" field from response and return the nodes that are marked by "type": type + return [node for node in response["nodes"] if node["type"].lower() == type.lower()] + @classmethod def __from_response(cls, response: Dict) -> Pipeline: """Converts response Json to 'Pipeline' object @@ -57,7 +62,9 @@ def __from_response(cls, response: Dict) -> Pipeline: """ if "api_key" not in response: response["api_key"] = config.TEAM_API_KEY - return Pipeline(response["id"], response["name"], response["api_key"]) + input = cls.__get_typed_nodes(response, "input") + output = cls.__get_typed_nodes(response, "output") + return Pipeline(response["id"], response["name"], response["api_key"], input=input, output=output) @classmethod def get(cls, pipeline_id: Text, api_key: Optional[Text] = None) -> Pipeline: @@ -73,7 +80,9 @@ def get(cls, pipeline_id: Text, api_key: Optional[Text] = None) -> Pipeline: resp = None try: url = urljoin(cls.backend_url, f"sdk/pipelines/{pipeline_id}") - if cls.aixplain_key != "": + if api_key is not None: + headers = {"Authorization": f"Token {api_key}", "Content-Type": "application/json"} + elif cls.aixplain_key != "": headers = {"x-aixplain-key": f"{cls.aixplain_key}", "Content-Type": "application/json"} else: headers = {"Authorization": f"Token {config.TEAM_API_KEY}", "Content-Type": "application/json"} diff --git a/pyproject.toml b/pyproject.toml index 112c8f9a..73980717 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ namespaces = true [project] name = "aiXplain" -version = "0.2.12" +version = "0.2.13rc2" description = "aiXplain SDK adds AI functions to software." readme = "README.md" requires-python = ">=3.5, <4" From f927ca4e2ac804421adc0fa946d46b4f293a1439 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Wed, 5 Jun 2024 17:46:40 -0300 Subject: [PATCH 5/8] Enabling pipeline tools --- aixplain/factories/agent_factory/__init__.py | 48 ++++++++++---- aixplain/factories/agent_factory/utils.py | 20 +++--- aixplain/modules/agent/__init__.py | 2 + aixplain/modules/agent/tool/__init__.py | 49 +++++++++++++++ aixplain/modules/agent/tool/model_tool.py | 66 ++++++++++++++++++++ aixplain/modules/agent/tool/pipeline_tool.py | 54 ++++++++++++++++ aixplain/modules/finetune/__init__.py | 5 +- aixplain/modules/metric.py | 3 - aixplain/modules/model.py | 2 +- 9 files changed, 220 insertions(+), 29 deletions(-) create mode 100644 aixplain/modules/agent/tool/__init__.py create mode 100644 aixplain/modules/agent/tool/model_tool.py create mode 100644 aixplain/modules/agent/tool/pipeline_tool.py diff --git a/aixplain/factories/agent_factory/__init__.py b/aixplain/factories/agent_factory/__init__.py index 0be2c145..ad4f29d1 100644 --- a/aixplain/factories/agent_factory/__init__.py +++ b/aixplain/factories/agent_factory/__init__.py @@ -26,6 +26,8 @@ from aixplain.enums.supplier import Supplier from aixplain.modules.agent import Agent, Tool +from aixplain.modules.agent.tool.model_tool import ModelTool +from aixplain.modules.agent.tool.pipeline_tool import PipelineTool from aixplain.utils import config from typing import Dict, List, Optional, Text, Union @@ -40,7 +42,7 @@ def create( name: Text, tools: List[Tool] = [], description: Text = "", - api_key: Optional[Text] = config.TEAM_API_KEY, + api_key: Text = config.TEAM_API_KEY, supplier: Union[Dict, Text, Supplier, int] = "aiXplain", version: Optional[Text] = None, cost: Optional[Dict] = None, @@ -57,25 +59,40 @@ def create( elif isinstance(supplier, Supplier): supplier = supplier.value["code"] + tool_payload = [] + for tool in tools: + if isinstance(tool, ModelTool): + tool_payload.append( + { + "function": tool.function.value, + "name": tool.name, + "description": tool.description, + "supplier": tool.supplier.value if tool.supplier else None, + } + ) + elif isinstance(tool, PipelineTool): + tool_payload.append( + { + "id": tool.pipeline, + "name": tool.name, + "description": tool.description, + } + ) + else: + raise Exception("Agent Creation Error: Tool type not supported.") + payload = { "name": name, - "tools": [ - { - "function": tool.function.value, - "name": tool.name, - "description": tool.description, - "supplier": tool.supplier.value if tool.supplier else None, - } - for tool in tools - ], + "api_key": api_key, + "tools": tool_payload, "description": description, "supplier": supplier, "version": version, "cost": cost, } - if llm_id is not None: payload["language_model_id"] = llm_id + logging.info(f"Start service for POST Create Agent - {url} - {headers} - {json.dumps(payload)}") r = _request_with_retry("post", url, headers=headers, data=json.dumps(payload)) if 200 <= r.status_code < 300: @@ -83,7 +100,14 @@ def create( asset_id = response["id"] agent = Agent( - id=asset_id, name=name, tools=tools, description=description, supplier=supplier, version=version, cost=cost + id=asset_id, + name=name, + tools=tools, + description=description, + supplier=supplier, + version=version, + cost=cost, + api_key=api_key, ) else: error_msg = "Agent Onboarding Error: Please contant the administrators." diff --git a/aixplain/factories/agent_factory/utils.py b/aixplain/factories/agent_factory/utils.py index f3fbdc1b..e7ecde0e 100644 --- a/aixplain/factories/agent_factory/utils.py +++ b/aixplain/factories/agent_factory/utils.py @@ -1,8 +1,6 @@ __author__ = "thiagocastroferreira" -from aixplain.enums.function import Function -from aixplain.enums.supplier import Supplier -from aixplain.modules.agent import Agent, Tool +from aixplain.modules.agent import Agent, ModelTool, PipelineTool from typing import Dict @@ -10,14 +8,13 @@ def build_agent(payload: Dict) -> Agent: """Instantiate a new agent in the platform.""" tools = payload["tools"] for i, tool in enumerate(tools): - function = Function(tool["function"]) - - try: - supplier = Supplier(tool["supplier"]) - except Exception: - supplier = None - - tools[i] = Tool(name=tool["name"], description=tool["description"], function=function, supplier=supplier) + if "function" in tool: + tool = ModelTool(**tool) + elif "id" in tool: + tool = PipelineTool(name=tool["name"], description=tool["description"], pipeline=tool["id"]) + else: + raise Exception("Agent Creation Error: Tool type not supported.") + tools[i] = tool agent = Agent( id=payload["id"], @@ -28,6 +25,7 @@ def build_agent(payload: Dict) -> Agent: version=payload["version"] if "version" in payload else None, cost=payload["cost"] if "cost" in payload else None, llm_id=payload["language_model_id"] if "language_model_id" in payload else None, + api_key=payload["api_key"], ) agent.url = "http://54.86.247.242:8000/async-execute" return agent diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index 11474666..2985d5a8 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -26,6 +26,8 @@ from aixplain.enums.supplier import Supplier from aixplain.modules.model import Model from aixplain.modules.agent.tool import Tool +from aixplain.modules.agent.tool.model_tool import ModelTool +from aixplain.modules.agent.tool.pipeline_tool import PipelineTool from typing import Dict, List, Text, Optional, Union from aixplain.utils import config diff --git a/aixplain/modules/agent/tool/__init__.py b/aixplain/modules/agent/tool/__init__.py new file mode 100644 index 00000000..64dd901f --- /dev/null +++ b/aixplain/modules/agent/tool/__init__.py @@ -0,0 +1,49 @@ +__author__ = "aiXplain" + +""" +Copyright 2024 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Lucas Pavanelli and Thiago Castro Ferreira +Date: May 16th 2024 +Description: + Agentification Class +""" +from abc import ABC +from typing import Text + + +class Tool(ABC): + """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. + + Attributes: + name (Text): name of the tool + description (Text): descriptiion of the tool + """ + + def __init__( + self, + name: Text, + description: Text, + **additional_info, + ) -> None: + """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. + + Args: + name (Text): name of the tool + description (Text): descriptiion of the tool + """ + self.name = name + self.description = description + self.additional_info = additional_info diff --git a/aixplain/modules/agent/tool/model_tool.py b/aixplain/modules/agent/tool/model_tool.py new file mode 100644 index 00000000..6796d298 --- /dev/null +++ b/aixplain/modules/agent/tool/model_tool.py @@ -0,0 +1,66 @@ +__author__ = "aiXplain" + +""" +Copyright 2024 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Lucas Pavanelli and Thiago Castro Ferreira +Date: May 16th 2024 +Description: + Agentification Class +""" +from typing import Text, Optional + +from aixplain.enums.function import Function +from aixplain.enums.supplier import Supplier +from aixplain.modules.agent.tool import Tool + + +class ModelTool(Tool): + """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. + + Attributes: + name (Text): name of the tool + description (Text): descriptiion of the tool + function (Function): task that the tool performs + supplier (Optional[Union[Dict, Text, Supplier, int]], optional): Preferred supplier to perform the task. Defaults to None. + """ + + def __init__( + self, + name: Text, + description: Text, + function: Function, + supplier: Optional[Supplier] = None, + **additional_info, + ) -> None: + """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. + + Args: + name (Text): name of the tool + description (Text): descriptiion of the tool + function (Function): task that the tool performs + supplier (Optional[Union[Dict, Text, Supplier, int]], optional): Preferred supplier to perform the task. Defaults to None. + """ + super().__init__(name, description, **additional_info) + if isinstance(function, str): + function = Function(function) + self.function = function + + try: + if isinstance(supplier, dict): + supplier = Supplier(supplier) + except Exception: + supplier = None + self.supplier = supplier diff --git a/aixplain/modules/agent/tool/pipeline_tool.py b/aixplain/modules/agent/tool/pipeline_tool.py new file mode 100644 index 00000000..c0e37b65 --- /dev/null +++ b/aixplain/modules/agent/tool/pipeline_tool.py @@ -0,0 +1,54 @@ +__author__ = "aiXplain" + +""" +Copyright 2024 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Lucas Pavanelli and Thiago Castro Ferreira +Date: May 16th 2024 +Description: + Agentification Class +""" +from typing import Text, Union + +from aixplain.modules.agent.tool import Tool +from aixplain.modules.pipeline import Pipeline + + +class PipelineTool(Tool): + """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. + + Attributes: + name (Text): name of the tool + description (Text): descriptiion of the tool + """ + + def __init__( + self, + name: Text, + description: Text, + pipeline: Union[Text, Pipeline], + **additional_info, + ) -> None: + """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. + + Args: + name (Text): name of the tool + description (Text): description of the tool + pipeline (Union[Text, Pipeline]): pipeline + """ + super().__init__(name, description, **additional_info) + if isinstance(pipeline, Pipeline): + pipeline = pipeline.id + self.pipeline = pipeline diff --git a/aixplain/modules/finetune/__init__.py b/aixplain/modules/finetune/__init__.py index e1b63941..fe2cb15c 100644 --- a/aixplain/modules/finetune/__init__.py +++ b/aixplain/modules/finetune/__init__.py @@ -26,7 +26,6 @@ from urllib.parse import urljoin from aixplain.modules.finetune.cost import FinetuneCost from aixplain.modules.finetune.hyperparameters import Hyperparameters -from aixplain.factories.model_factory import ModelFactory from aixplain.modules.asset import Asset from aixplain.modules.dataset import Dataset from aixplain.modules.model import Model @@ -110,7 +109,7 @@ def start(self) -> Model: """ payload = {} try: - url = urljoin(self.backend_url, f"sdk/finetune") + url = urljoin(self.backend_url, "sdk/finetune") headers = {"Authorization": f"Token {self.api_key}", "Content-Type": "application/json"} payload = { "name": self.name, @@ -134,6 +133,8 @@ def start(self) -> Model: r = _request_with_retry("post", url, headers=headers, json=payload) resp = r.json() logging.info(f"Response for POST Start FineTune - Name: {self.name} / Status {resp}") + from aixplain.factories.model_factory import ModelFactory + return ModelFactory().get(resp["id"]) except Exception: message = "" diff --git a/aixplain/modules/metric.py b/aixplain/modules/metric.py index d591772b..86c08a08 100644 --- a/aixplain/modules/metric.py +++ b/aixplain/modules/metric.py @@ -24,9 +24,6 @@ from typing import Optional, Text, List, Union from aixplain.modules.asset import Asset -from aixplain.utils.file_utils import _request_with_retry -from aixplain.factories.model_factory import ModelFactory - class Metric(Asset): """Represents a metric to be computed on one or more peices of data. It is usually linked to a machine learning task. diff --git a/aixplain/modules/model.py b/aixplain/modules/model.py index 7679e798..be468dac 100644 --- a/aixplain/modules/model.py +++ b/aixplain/modules/model.py @@ -24,7 +24,6 @@ import json import logging import traceback -from aixplain.factories.file_factory import FileFactory from aixplain.enums import Supplier from aixplain.modules.asset import Asset from aixplain.utils import config @@ -217,6 +216,7 @@ def run_async(self, data: Union[Text, Dict], name: Text = "model_process", param dict: polling URL in response """ headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} + from aixplain.factories.file_factory import FileFactory data = FileFactory.to_link(data) if isinstance(data, dict): From 4d95e90d41f6436a15433a44f97714619319e5df Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira <85182544+thiago-aixplain@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:07:30 -0300 Subject: [PATCH 6/8] M 6875703542 agentification deployment (#195) * First changes for agent integration with backend * Official creation and deletion services * Running agent method --------- Co-authored-by: Thiago Castro Ferreira --- aixplain/factories/agent_factory/__init__.py | 100 +++++++++-------- aixplain/factories/agent_factory/utils.py | 37 +++++-- aixplain/modules/agent/__init__.py | 111 ++++++++++++++++++- aixplain/modules/agent/tool/__init__.py | 6 +- aixplain/modules/agent/tool/model_tool.py | 4 +- aixplain/modules/agent/tool/pipeline_tool.py | 5 +- aixplain/modules/asset.py | 8 +- aixplain/modules/model.py | 6 +- 8 files changed, 204 insertions(+), 73 deletions(-) diff --git a/aixplain/factories/agent_factory/__init__.py b/aixplain/factories/agent_factory/__init__.py index ad4f29d1..0a543d77 100644 --- a/aixplain/factories/agent_factory/__init__.py +++ b/aixplain/factories/agent_factory/__init__.py @@ -33,6 +33,7 @@ from aixplain.factories.agent_factory.utils import build_agent from aixplain.utils.file_utils import _request_with_retry +from urllib.parse import urljoin class AgentFactory: @@ -45,14 +46,13 @@ def create( api_key: Text = config.TEAM_API_KEY, supplier: Union[Dict, Text, Supplier, int] = "aiXplain", version: Optional[Text] = None, - cost: Optional[Dict] = None, llm_id: Optional[Text] = None, ) -> Agent: """Create a new agent in the platform.""" try: agent = None - url = "http://54.86.247.242:8000/create" - headers = {"Authorization": "token " + api_key} + url = urljoin(config.BACKEND_URL, "sdk/agents") + headers = {"x-api-key": api_key} if isinstance(supplier, dict): supplier = supplier["code"] @@ -65,17 +65,18 @@ def create( tool_payload.append( { "function": tool.function.value, - "name": tool.name, + "type": "model", "description": tool.description, - "supplier": tool.supplier.value if tool.supplier else None, + "supplier": tool.supplier.value["code"] if tool.supplier else None, + "version": tool.version if tool.version else None, } ) elif isinstance(tool, PipelineTool): tool_payload.append( { - "id": tool.pipeline, - "name": tool.name, + "assetId": tool.pipeline, "description": tool.description, + "type": "pipeline", } ) else: @@ -83,34 +84,29 @@ def create( payload = { "name": name, - "api_key": api_key, - "tools": tool_payload, + "assets": tool_payload, "description": description, "supplier": supplier, "version": version, - "cost": cost, } if llm_id is not None: - payload["language_model_id"] = llm_id + payload["llmId"] = llm_id logging.info(f"Start service for POST Create Agent - {url} - {headers} - {json.dumps(payload)}") - r = _request_with_retry("post", url, headers=headers, data=json.dumps(payload)) + r = _request_with_retry("post", url, headers=headers, json=payload) if 200 <= r.status_code < 300: response = r.json() - - asset_id = response["id"] - agent = Agent( - id=asset_id, - name=name, - tools=tools, - description=description, - supplier=supplier, - version=version, - cost=cost, - api_key=api_key, - ) + agent = build_agent(payload=response, api_key=api_key) else: + error = r.json() error_msg = "Agent Onboarding Error: Please contant the administrators." + if "message" in error: + msg = error["message"] + if error["message"] == "err.name_already_exists": + msg = "Agent name already exists." + elif error["message"] == "err.asset_is_not_available": + msg = "Some the tools are not available." + error_msg = f"Agent Onboarding Error (HTTP {r.status_code}): {msg}" logging.exception(error_msg) raise Exception(error_msg) except Exception as e: @@ -120,35 +116,51 @@ def create( @classmethod def list(cls) -> Dict: """List all agents available in the platform.""" - url = "http://54.86.247.242:8000/list" - if config.AIXPLAIN_API_KEY != "": - headers = {"x-aixplain-key": f"{config.AIXPLAIN_API_KEY}", "Content-Type": "application/json"} - else: - headers = {"Authorization": f"Token {config.TEAM_API_KEY}", "Content-Type": "application/json"} + url = urljoin(config.BACKEND_URL, "sdk/agents") + headers = {"x-api-key": config.TEAM_API_KEY, "Content-Type": "application/json"} payload = {} - logging.info(f"Start service for POST List Agents - {url} - {headers} - {json.dumps(payload)}") - r = _request_with_retry("get", url, headers=headers) - resp = r.json() + logging.info(f"Start service for GET List Agents - {url} - {headers} - {json.dumps(payload)}") + try: + r = _request_with_retry("get", url, headers=headers) + resp = r.json() - agents, page_total, total = [], 0, 0 - results = resp - page_total = len(results) - total = len(results) - logging.info(f"Response for POST List Dataset - Page Total: {page_total} / Total: {total}") - for agent in results: - agents.append(build_agent(agent)) - return {"results": agents, "page_total": page_total, "page_number": 0, "total": total} + if 200 <= r.status_code < 300: + agents, page_total, total = [], 0, 0 + results = resp + page_total = len(results) + total = len(results) + logging.info(f"Response for GET List Agents - Page Total: {page_total} / Total: {total}") + for agent in results: + agents.append(build_agent(agent)) + return {"results": agents, "page_total": page_total, "page_number": 0, "total": total} + else: + error_msg = "Agent Listing Error: Please contant the administrators." + if "message" in resp: + msg = resp["message"] + error_msg = f"Agent Listing Error (HTTP {r.status_code}): {msg}" + logging.exception(error_msg) + raise Exception(error_msg) + except Exception as e: + raise Exception(e) @classmethod - def get(cls, agent_id: Text) -> Agent: + def get(cls, agent_id: Text, api_key: Optional[Text] = None) -> Agent: """Get agent by id.""" - url = f"http://54.86.247.242:8000/get?id={agent_id}" + url = urljoin(config.BACKEND_URL, f"sdk/agents/{agent_id}") if config.AIXPLAIN_API_KEY != "": headers = {"x-aixplain-key": f"{config.AIXPLAIN_API_KEY}", "Content-Type": "application/json"} else: - headers = {"Authorization": f"Token {config.TEAM_API_KEY}", "Content-Type": "application/json"} + api_key = api_key if api_key is not None else config.TEAM_API_KEY + headers = {"x-api-key": api_key, "Content-Type": "application/json"} logging.info(f"Start service for GET Agent - {url} - {headers}") r = _request_with_retry("get", url, headers=headers) resp = r.json() - return build_agent(resp) + if 200 <= r.status_code < 300: + return build_agent(resp) + else: + msg = "Please contant the administrators." + if "message" in resp: + msg = resp["message"] + error_msg = f"Agent Get Error (HTTP {r.status_code}): {msg}" + raise Exception(error_msg) diff --git a/aixplain/factories/agent_factory/utils.py b/aixplain/factories/agent_factory/utils.py index e7ecde0e..410d1692 100644 --- a/aixplain/factories/agent_factory/utils.py +++ b/aixplain/factories/agent_factory/utils.py @@ -1,17 +1,31 @@ __author__ = "thiagocastroferreira" +import aixplain.utils.config as config +from aixplain.enums import Function, Supplier +from aixplain.enums.asset_status import AssetStatus from aixplain.modules.agent import Agent, ModelTool, PipelineTool -from typing import Dict +from typing import Dict, Text +from urllib.parse import urljoin -def build_agent(payload: Dict) -> Agent: +def build_agent(payload: Dict, api_key: Text = config.TEAM_API_KEY) -> Agent: """Instantiate a new agent in the platform.""" - tools = payload["tools"] + tools = payload["assets"] for i, tool in enumerate(tools): - if "function" in tool: - tool = ModelTool(**tool) - elif "id" in tool: - tool = PipelineTool(name=tool["name"], description=tool["description"], pipeline=tool["id"]) + if tool["type"] == "model": + for supplier in Supplier: + if tool["supplier"].lower() in [supplier.value["code"].lower(), supplier.value["name"].lower()]: + tool["supplier"] = supplier + break + + tool = ModelTool( + description=tool["description"], + function=Function(tool["function"]), + supplier=tool["supplier"], + version=tool["version"], + ) + elif tool["type"] == "pipeline": + tool = PipelineTool(description=tool["description"], pipeline=tool["assetId"]) else: raise Exception("Agent Creation Error: Tool type not supported.") tools[i] = tool @@ -21,11 +35,12 @@ def build_agent(payload: Dict) -> Agent: name=payload["name"] if "name" in payload else "", tools=tools, description=payload["description"] if "description" in payload else "", - supplier=payload["supplier"] if "supplier" in payload else None, + supplier=payload["teamId"] if "teamId" in payload else None, version=payload["version"] if "version" in payload else None, cost=payload["cost"] if "cost" in payload else None, - llm_id=payload["language_model_id"] if "language_model_id" in payload else None, - api_key=payload["api_key"], + llm_id=payload["llmId"] if "llmId" in payload else "6646261c6eb563165658bbb1", + api_key=api_key, + status=AssetStatus(payload["status"]), ) - agent.url = "http://54.86.247.242:8000/async-execute" + agent.url = urljoin(config.BACKEND_URL, f"sdk/agents/{agent.id}/run") return agent diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index 2985d5a8..ca9df4b5 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -20,15 +20,20 @@ Description: Agentification Class """ +import json import logging +import time +import traceback from aixplain.utils.file_utils import _request_with_retry from aixplain.enums.supplier import Supplier +from aixplain.enums.asset_status import AssetStatus from aixplain.modules.model import Model from aixplain.modules.agent.tool import Tool from aixplain.modules.agent.tool.model_tool import ModelTool from aixplain.modules.agent.tool.pipeline_tool import PipelineTool from typing import Dict, List, Text, Optional, Union +from urllib.parse import urljoin from aixplain.utils import config @@ -41,6 +46,7 @@ class Agent(Model): name (Text): Name of the Agent tools (List[Tool]): List of tools that the Agent uses. description (Text, optional): description of the Agent. Defaults to "". + llm_id (Text, optional): large language model. Defaults to GPT-4o (6646261c6eb563165658bbb1). supplier (Text): Supplier of the Agent. version (Text): Version of the Agent. backend_url (str): URL of the backend. @@ -54,11 +60,12 @@ def __init__( name: Text, tools: List[Tool] = [], description: Text = "", + llm_id: Text = "6646261c6eb563165658bbb1", api_key: Optional[Text] = config.TEAM_API_KEY, supplier: Union[Dict, Text, Supplier, int] = "aiXplain", version: Optional[Text] = None, cost: Optional[Dict] = None, - llm_id: Optional[Text] = None, + status: AssetStatus = AssetStatus.ONBOARDING, **additional_info, ) -> None: """Create a FineTune with the necessary information. @@ -68,29 +75,121 @@ def __init__( name (Text): Name of the Agent tools (List[Tool]): List of tools that the Agent uses. description (Text, optional): description of the Agent. Defaults to "". + llm_id (Text, optional): large language model. Defaults to GPT-4o (6646261c6eb563165658bbb1). supplier (Text): Supplier of the Agent. version (Text): Version of the Agent. backend_url (str): URL of the backend. api_key (str): The TEAM API key used for authentication. cost (Dict, optional): model price. Defaults to None. - **additional_info: Additional information to be saved with the FineTune. """ assert len(tools) > 0, "At least one tool must be provided." super().__init__(id, name, description, api_key, supplier, version, cost=cost) self.additional_info = additional_info self.tools = tools self.llm_id = llm_id + if isinstance(status, str): + try: + status = AssetStatus(status) + except Exception: + status = AssetStatus.ONBOARDING + self.status = status + + def run( + self, + query: Text, + session_id: Optional[Text] = None, + history: Optional[List[Dict]] = None, + name: Text = "model_process", + timeout: float = 300, + parameters: Dict = {}, + wait_time: float = 0.5, + ) -> Dict: + """Runs an agent call. + + Args: + query (Text): query to be processed by the agent. + session_id (Optional[Text], optional): conversation Session ID. Defaults to None. + history (Optional[List[Dict]], optional): chat history (in case session ID is None). Defaults to None. + name (Text, optional): ID given to a call. Defaults to "model_process". + timeout (float, optional): total polling time. Defaults to 300. + parameters (Dict, optional): optional parameters to the model. Defaults to "{}". + wait_time (float, optional): wait time in seconds between polling calls. Defaults to 0.5. + + Returns: + Dict: parsed output from model + """ + start = time.time() + try: + response = self.run_async(query=query, session_id=session_id, history=history, name=name, parameters=parameters) + if response["status"] == "FAILED": + end = time.time() + response["elapsed_time"] = end - start + return response + poll_url = response["url"] + end = time.time() + response = self.sync_poll(poll_url, name=name, timeout=timeout, wait_time=wait_time) + return response + except Exception as e: + msg = f"Error in request for {name} - {traceback.format_exc()}" + logging.error(f"Model Run: Error in running for {name}: {e}") + end = time.time() + return {"status": "FAILED", "error": msg, "elapsed_time": end - start} + + def run_async( + self, + query: Text, + session_id: Optional[Text] = None, + history: Optional[List[Dict]] = None, + name: Text = "model_process", + parameters: Dict = {}, + ) -> Dict: + """Runs asynchronously an agent call. + + Args: + query (Text): query to be processed by the agent. + session_id (Optional[Text], optional): conversation Session ID. Defaults to None. + history (Optional[List[Dict]], optional): chat history (in case session ID is None). Defaults to None. + name (Text, optional): ID given to a call. Defaults to "model_process". + parameters (Dict, optional): optional parameters to the model. Defaults to "{}". + + Returns: + dict: polling URL in response + """ + headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} + from aixplain.factories.file_factory import FileFactory + + payload = {"id": self.id, "query": FileFactory.to_link(query), "sessionId": session_id, "history": history} + payload.update(parameters) + payload = json.dumps(payload) + + r = _request_with_retry("post", self.url, headers=headers, data=payload) + logging.info(f"Model Run Async: Start service for {name} - {self.url} - {payload} - {headers}") + + resp = None + try: + resp = r.json() + logging.info(f"Result of request for {name} - {r.status_code} - {resp}") + + poll_url = resp["data"] + response = {"status": "IN_PROGRESS", "url": poll_url} + except Exception: + response = {"status": "FAILED"} + msg = f"Error in request for {name} - {traceback.format_exc()}" + logging.error(f"Model Run Async: Error in running for {name}: {resp}") + if resp is not None: + response["error"] = msg + return response def delete(self) -> None: """Delete Corpus service""" try: - url = f"http://54.86.247.242:8000/delete/{self.id}" - headers = {"Authorization": f"Token {config.TEAM_API_KEY}", "Content-Type": "application/json"} - logging.info(f"Start service for DELETE Agent - {url} - {headers}") + url = urljoin(config.BACKEND_URL, f"sdk/agents/{self.id}") + headers = {"x-api-key": config.TEAM_API_KEY, "Content-Type": "application/json"} + logging.debug(f"Start service for DELETE Agent - {url} - {headers}") r = _request_with_retry("delete", url, headers=headers) if r.status_code != 200: raise Exception() except Exception: - message = "Agent Deletion Error: Make sure the agent exists and you are the owner." + message = f"Agent Deletion Error (HTTP {r.status_code}): Make sure the agent exists and you are the owner." logging.error(message) raise Exception(f"{message}") diff --git a/aixplain/modules/agent/tool/__init__.py b/aixplain/modules/agent/tool/__init__.py index 64dd901f..2a22511a 100644 --- a/aixplain/modules/agent/tool/__init__.py +++ b/aixplain/modules/agent/tool/__init__.py @@ -21,7 +21,7 @@ Agentification Class """ from abc import ABC -from typing import Text +from typing import Optional, Text class Tool(ABC): @@ -30,12 +30,14 @@ class Tool(ABC): Attributes: name (Text): name of the tool description (Text): descriptiion of the tool + version (Text): version of the tool """ def __init__( self, name: Text, description: Text, + version: Optional[Text] = None, **additional_info, ) -> None: """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. @@ -43,7 +45,9 @@ def __init__( Args: name (Text): name of the tool description (Text): descriptiion of the tool + version (Text): version of the tool """ self.name = name self.description = description + self.version = version self.additional_info = additional_info diff --git a/aixplain/modules/agent/tool/model_tool.py b/aixplain/modules/agent/tool/model_tool.py index 6796d298..38c03fe7 100644 --- a/aixplain/modules/agent/tool/model_tool.py +++ b/aixplain/modules/agent/tool/model_tool.py @@ -31,7 +31,6 @@ class ModelTool(Tool): """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. Attributes: - name (Text): name of the tool description (Text): descriptiion of the tool function (Function): task that the tool performs supplier (Optional[Union[Dict, Text, Supplier, int]], optional): Preferred supplier to perform the task. Defaults to None. @@ -39,7 +38,6 @@ class ModelTool(Tool): def __init__( self, - name: Text, description: Text, function: Function, supplier: Optional[Supplier] = None, @@ -53,7 +51,7 @@ def __init__( function (Function): task that the tool performs supplier (Optional[Union[Dict, Text, Supplier, int]], optional): Preferred supplier to perform the task. Defaults to None. """ - super().__init__(name, description, **additional_info) + super().__init__("", description, **additional_info) if isinstance(function, str): function = Function(function) self.function = function diff --git a/aixplain/modules/agent/tool/pipeline_tool.py b/aixplain/modules/agent/tool/pipeline_tool.py index c0e37b65..330d4c67 100644 --- a/aixplain/modules/agent/tool/pipeline_tool.py +++ b/aixplain/modules/agent/tool/pipeline_tool.py @@ -30,13 +30,11 @@ class PipelineTool(Tool): """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. Attributes: - name (Text): name of the tool description (Text): descriptiion of the tool """ def __init__( self, - name: Text, description: Text, pipeline: Union[Text, Pipeline], **additional_info, @@ -44,11 +42,10 @@ def __init__( """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. Args: - name (Text): name of the tool description (Text): description of the tool pipeline (Union[Text, Pipeline]): pipeline """ - super().__init__(name, description, **additional_info) + super().__init__("", description, **additional_info) if isinstance(pipeline, Pipeline): pipeline = pipeline.id self.pipeline = pipeline diff --git a/aixplain/modules/asset.py b/aixplain/modules/asset.py index 52b79912..c453415d 100644 --- a/aixplain/modules/asset.py +++ b/aixplain/modules/asset.py @@ -57,7 +57,13 @@ def __init__( elif isinstance(supplier, Dict) is True: self.supplier = Supplier(supplier) else: - self.supplier = supplier + self.supplier = None + for supplier_ in Supplier: + if supplier.lower() in [supplier_.value["code"].lower(), supplier_.value["name"].lower()]: + self.supplier = supplier_ + break + if self.supplier is None: + self.supplier = supplier except Exception: self.supplier = str(supplier) self.version = version diff --git a/aixplain/modules/model.py b/aixplain/modules/model.py index be468dac..e1e13c63 100644 --- a/aixplain/modules/model.py +++ b/aixplain/modules/model.py @@ -100,7 +100,7 @@ def __repr__(self): except Exception: return f"" - def __polling(self, poll_url: Text, name: Text = "model_process", wait_time: float = 0.5, timeout: float = 300) -> Dict: + def sync_poll(self, poll_url: Text, name: Text = "model_process", wait_time: float = 0.5, timeout: float = 300) -> Dict: """Keeps polling the platform to check whether an asynchronous call is done. Args: @@ -161,7 +161,7 @@ def poll(self, poll_url: Text, name: Text = "model_process") -> Dict: resp["status"] = "FAILED" else: resp["status"] = "IN_PROGRESS" - logging.info(f"Single Poll for Model: Status of polling for {name}: {resp}") + logging.debug(f"Single Poll for Model: Status of polling for {name}: {resp}") except Exception as e: resp = {"status": "FAILED"} logging.error(f"Single Poll for Model: Error of polling for {name}: {e}") @@ -196,7 +196,7 @@ def run( return response poll_url = response["url"] end = time.time() - response = self.__polling(poll_url, name=name, timeout=timeout, wait_time=wait_time) + response = self.sync_poll(poll_url, name=name, timeout=timeout, wait_time=wait_time) return response except Exception as e: msg = f"Error in request for {name} - {traceback.format_exc()}" From d58a69e3beec4eebc357f22db0e47749a3ee7630 Mon Sep 17 00:00:00 2001 From: Lucas Pavanelli Date: Tue, 2 Jul 2024 19:16:14 +0000 Subject: [PATCH 7/8] Fix bug when supplier and tools are not given --- aixplain/factories/agent_factory/utils.py | 2 +- aixplain/modules/agent/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aixplain/factories/agent_factory/utils.py b/aixplain/factories/agent_factory/utils.py index 410d1692..014b14fc 100644 --- a/aixplain/factories/agent_factory/utils.py +++ b/aixplain/factories/agent_factory/utils.py @@ -14,7 +14,7 @@ def build_agent(payload: Dict, api_key: Text = config.TEAM_API_KEY) -> Agent: for i, tool in enumerate(tools): if tool["type"] == "model": for supplier in Supplier: - if tool["supplier"].lower() in [supplier.value["code"].lower(), supplier.value["name"].lower()]: + if tool["supplier"] is not None and tool["supplier"].lower() in [supplier.value["code"].lower(), supplier.value["name"].lower()]: tool["supplier"] = supplier break diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index ca9df4b5..2f244d56 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -82,7 +82,7 @@ def __init__( api_key (str): The TEAM API key used for authentication. cost (Dict, optional): model price. Defaults to None. """ - assert len(tools) > 0, "At least one tool must be provided." + # assert len(tools) > 0, "At least one tool must be provided." super().__init__(id, name, description, api_key, supplier, version, cost=cost) self.additional_info = additional_info self.tools = tools From b1b9f068600b8400dde720add86c5f4b81b165fb Mon Sep 17 00:00:00 2001 From: Lucas Pavanelli <86805709+lucas-aixplain@users.noreply.github.com> Date: Sat, 13 Jul 2024 18:21:16 -0300 Subject: [PATCH 8/8] Add agents functional tests (#204) --- aixplain/factories/agent_factory/__init__.py | 2 +- aixplain/factories/agent_factory/utils.py | 6 +- aixplain/modules/agent/tool/model_tool.py | 8 +- aixplain/modules/agent/tool/pipeline_tool.py | 1 + .../functional/agent/agent_functional_test.py | 75 +++++++++++++++++++ .../agent/data/agent_test_end2end.json | 14 ++++ 6 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 tests/functional/agent/agent_functional_test.py create mode 100644 tests/functional/agent/data/agent_test_end2end.json diff --git a/aixplain/factories/agent_factory/__init__.py b/aixplain/factories/agent_factory/__init__.py index 0a543d77..36380a76 100644 --- a/aixplain/factories/agent_factory/__init__.py +++ b/aixplain/factories/agent_factory/__init__.py @@ -41,12 +41,12 @@ class AgentFactory: def create( cls, name: Text, + llm_id: Text, tools: List[Tool] = [], description: Text = "", api_key: Text = config.TEAM_API_KEY, supplier: Union[Dict, Text, Supplier, int] = "aiXplain", version: Optional[Text] = None, - llm_id: Optional[Text] = None, ) -> Agent: """Create a new agent in the platform.""" try: diff --git a/aixplain/factories/agent_factory/utils.py b/aixplain/factories/agent_factory/utils.py index 014b14fc..6363a08e 100644 --- a/aixplain/factories/agent_factory/utils.py +++ b/aixplain/factories/agent_factory/utils.py @@ -14,12 +14,14 @@ def build_agent(payload: Dict, api_key: Text = config.TEAM_API_KEY) -> Agent: for i, tool in enumerate(tools): if tool["type"] == "model": for supplier in Supplier: - if tool["supplier"] is not None and tool["supplier"].lower() in [supplier.value["code"].lower(), supplier.value["name"].lower()]: + if tool["supplier"] is not None and tool["supplier"].lower() in [ + supplier.value["code"].lower(), + supplier.value["name"].lower(), + ]: tool["supplier"] = supplier break tool = ModelTool( - description=tool["description"], function=Function(tool["function"]), supplier=tool["supplier"], version=tool["version"], diff --git a/aixplain/modules/agent/tool/model_tool.py b/aixplain/modules/agent/tool/model_tool.py index 38c03fe7..69bf28d5 100644 --- a/aixplain/modules/agent/tool/model_tool.py +++ b/aixplain/modules/agent/tool/model_tool.py @@ -20,7 +20,7 @@ Description: Agentification Class """ -from typing import Text, Optional +from typing import Optional from aixplain.enums.function import Function from aixplain.enums.supplier import Supplier @@ -31,14 +31,12 @@ class ModelTool(Tool): """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. Attributes: - description (Text): descriptiion of the tool function (Function): task that the tool performs supplier (Optional[Union[Dict, Text, Supplier, int]], optional): Preferred supplier to perform the task. Defaults to None. """ def __init__( self, - description: Text, function: Function, supplier: Optional[Supplier] = None, **additional_info, @@ -46,12 +44,10 @@ def __init__( """Specialized software or resource designed to assist the AI in executing specific tasks or functions based on user commands. Args: - name (Text): name of the tool - description (Text): descriptiion of the tool function (Function): task that the tool performs supplier (Optional[Union[Dict, Text, Supplier, int]], optional): Preferred supplier to perform the task. Defaults to None. """ - super().__init__("", description, **additional_info) + super().__init__("", "", **additional_info) if isinstance(function, str): function = Function(function) self.function = function diff --git a/aixplain/modules/agent/tool/pipeline_tool.py b/aixplain/modules/agent/tool/pipeline_tool.py index 330d4c67..a517b198 100644 --- a/aixplain/modules/agent/tool/pipeline_tool.py +++ b/aixplain/modules/agent/tool/pipeline_tool.py @@ -31,6 +31,7 @@ class PipelineTool(Tool): Attributes: description (Text): descriptiion of the tool + pipeline (Union[Text, Pipeline]): pipeline """ def __init__( diff --git a/tests/functional/agent/agent_functional_test.py b/tests/functional/agent/agent_functional_test.py new file mode 100644 index 00000000..f58dcb63 --- /dev/null +++ b/tests/functional/agent/agent_functional_test.py @@ -0,0 +1,75 @@ +__author__ = "lucaspavanelli" + +""" +Copyright 2022 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +import json +from dotenv import load_dotenv + +load_dotenv() +from aixplain.factories import AgentFactory +from aixplain.modules.agent import ModelTool, PipelineTool +from aixplain.enums.supplier import Supplier + +import pytest + +RUN_FILE = "tests/functional/agent/data/agent_test_end2end.json" + + +def read_data(data_path): + return json.load(open(data_path, "r")) + + +@pytest.fixture(scope="module", params=read_data(RUN_FILE)) +def run_input_map(request): + return request.param + + +def test_end2end(run_input_map): + tools = [] + if "model_tools" in run_input_map: + for tool in run_input_map["model_tools"]: + for supplier in Supplier: + if tool["supplier"] is not None and tool["supplier"].lower() in [ + supplier.value["code"].lower(), + supplier.value["name"].lower(), + ]: + tool["supplier"] = supplier + break + tools.append(ModelTool(function=tool["function"], supplier=tool["supplier"])) + if "pipeline_tools" in run_input_map: + for tool in run_input_map["pipeline_tools"]: + tools.append(PipelineTool(description=tool["description"], pipeline=tool["pipeline_id"])) + print(f"Creating agent with tools: {tools}") + agent = AgentFactory.create(name=run_input_map["agent_name"], llm_id=run_input_map["llm_id"], tools=tools) + print(f"Agent created: {agent.__dict__}") + print("Running agent") + response = agent.run(query=run_input_map["query"]) + print(f"Agent response: {response}") + assert response is not None + assert response["completed"] is True + assert response["status"].lower() == "success" + assert "data" in response + assert response["data"]["session_id"] is not None + assert response["data"]["output"] is not None + print("Deleting agent") + agent.delete() + + +def test_list_agents(): + agents = AgentFactory.list() + assert "results" in agents + agents_result = agents["results"] + assert type(agents_result) is list diff --git a/tests/functional/agent/data/agent_test_end2end.json b/tests/functional/agent/data/agent_test_end2end.json new file mode 100644 index 00000000..147928fe --- /dev/null +++ b/tests/functional/agent/data/agent_test_end2end.json @@ -0,0 +1,14 @@ +[ + { + "agent_name": "[TEST] Translation agent", + "llm_id": "6626a3a8c8f1d089790cf5a2", + "llm_name": "Groq Llama 3 70B", + "query": "Who is the president of Brazil right now? Translate to pt", + "model_tools": [ + { + "function": "translation", + "supplier": "AWS" + } + ] + } +]