From 727e9f321b25c84cd11a4dee0fa639d1e6d9f4cd Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Mon, 30 Sep 2024 03:10:45 +0000 Subject: [PATCH 01/21] Add workflow executor example Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/README.md | 35 ++++++ .../docker_compose/docker_compose.yaml | 27 +++++ .../tools/components/component.py | 6 + .../tools/components/workflow.py | 68 +++++++++++ WorkflowExecAgent/tools/sdk.py | 8 ++ WorkflowExecAgent/tools/tools.py | 17 +++ WorkflowExecAgent/tools/tools.yaml | 29 +++++ .../tools/utils/handle_requests.py | 106 ++++++++++++++++++ WorkflowExecAgent/tools/utils/logger.py | 13 +++ 9 files changed, 309 insertions(+) create mode 100644 WorkflowExecAgent/README.md create mode 100644 WorkflowExecAgent/docker_compose/docker_compose.yaml create mode 100644 WorkflowExecAgent/tools/components/component.py create mode 100644 WorkflowExecAgent/tools/components/workflow.py create mode 100644 WorkflowExecAgent/tools/sdk.py create mode 100644 WorkflowExecAgent/tools/tools.py create mode 100644 WorkflowExecAgent/tools/tools.yaml create mode 100644 WorkflowExecAgent/tools/utils/handle_requests.py create mode 100644 WorkflowExecAgent/tools/utils/logger.py diff --git a/WorkflowExecAgent/README.md b/WorkflowExecAgent/README.md new file mode 100644 index 0000000000..e8eb6b787e --- /dev/null +++ b/WorkflowExecAgent/README.md @@ -0,0 +1,35 @@ +# Workflow Executor Agent + +## + +## Setup Guide + +Workflow Executor will have a single docker image. + +Instructions to setup. + +```sh +git clone https://github.com/opea-project/GenAIExamples.git +cd GenAIExamples/WorkflowExecutor/ +docker build -t opea/workflow-executor:latest -f Dockerfile . +``` + +Configure .env file with the following. Replace the variables according to your usecase. + +```sh +export ip_address=$(hostname -I | awk '{print $1}') +export SERVING_PORT=8000 +export LLM_MODEL="mistralai/Mistral-7B-Instruct-v0.3" +export HUGGINGFACEHUB_API_TOKEN=${HF_TOKEN} +export SDK_BASE_URL=${SDK_BASE_URL} +export SERVING_TOKEN=${SERVING_TOKEN} +export http_proxy=${http_proxy} +export https_proxy=${https_proxy} +export llm_serving_url= +``` + +Launch service: + +```sh +docker compose -f compose.yaml up -d +``` \ No newline at end of file diff --git a/WorkflowExecAgent/docker_compose/docker_compose.yaml b/WorkflowExecAgent/docker_compose/docker_compose.yaml new file mode 100644 index 0000000000..02af9d45d0 --- /dev/null +++ b/WorkflowExecAgent/docker_compose/docker_compose.yaml @@ -0,0 +1,27 @@ +services: + supervisor: + image: opea/agent-langchain:latest + container_name: supervisor-agent-endpoint + volumes: + - ${WORKDIR}/GenAIComps/comps/agent/langchain/:/home/user/comps/agent/langchain/ + - ${TOOLSET_PATH}:/home/user/tools/ + ports: + - "9090:9090" + ipc: host + environment: + ip_address: ${ip_address} + strategy: workflow_executor + recursion_limit: ${recursion_limit_supervisor} + llm_engine: openai + OPENAI_API_KEY: ${OPENAI_API_KEY} + model: ${model} + temperature: ${temperature} + max_new_tokens: ${max_new_tokens} + streaming: false + tools: /home/user/tools/tools.yaml + no_proxy: ${no_proxy} + http_proxy: ${http_proxy} + https_proxy: ${https_proxy} + port: 9090 + SDK_BASE_URL: ${SDK_BASE_URL} + SERVING_TOKEN: ${SERVING_TOKEN} diff --git a/WorkflowExecAgent/tools/components/component.py b/WorkflowExecAgent/tools/components/component.py new file mode 100644 index 0000000000..c90efd0f4a --- /dev/null +++ b/WorkflowExecAgent/tools/components/component.py @@ -0,0 +1,6 @@ +class Component(): + def __init__(self, request_handler): + self.request_handler = request_handler + + def _make_request(self, *args, **kwargs): + return self.request_handler._make_request(*args, **kwargs) diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py new file mode 100644 index 0000000000..6ea78cfa8e --- /dev/null +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -0,0 +1,68 @@ +import json +from typing import Dict + +from .component import Component + +class Workflow(Component): + """ + Class for handling EasyData workflow operations. + + Attributes: + workflow_id: workflow id + wf_key: workflow key. Generated and stored when starting a servable workflow. + """ + + def __init__(self, request_handler, workflow_id=None, workflow_key=None): + super().__init__(request_handler) + self.workflow_id = workflow_id + self.wf_key = workflow_key + + def start(self, params: Dict[str, str]) -> Dict[str, str]: + """ + ``POST https://SDK_BASE_URL/serving/servable_workflows/{workflow_id}/start`` + + Starts a workflow with the workflow_id. + + :param string workflow_id: Workflow id to start. + + :returns: WorkflowKey + + :rtype: dict + """ + data = json.dumps({"params": params}) + endpoint = f'serving/servable_workflows/{self.workflow_id}/start' + wf_key = self._make_request(endpoint, "POST", data)["wf_key"] + if wf_key: + return {"message": f"Workflow successfully started. The workflow key is {wf_key}"} + else: + return {"message": "Workflow failed to start"} + + def get_status(self) -> Dict[str, str]: + """ + ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/status`` + + Gets the workflow status. + + :param string workflow_key: Workflow id to retrieve status. + + :returns: Status: Dictionary of presets + + :rtype: json object + """ + + endpoint = f'serving/serving_workflows/{self.wf_key}/status' + return self._make_request(endpoint, "GET") + + def result(self) -> list[Dict[str, str]]: + """ + ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/results`` + + Gets the result. + + :returns: + + :rtype: json object + """ + + endpoint = f'serving/serving_workflows/{self.wf_key}/results' + return self._make_request(endpoint, "GET") diff --git a/WorkflowExecAgent/tools/sdk.py b/WorkflowExecAgent/tools/sdk.py new file mode 100644 index 0000000000..d1bc10651a --- /dev/null +++ b/WorkflowExecAgent/tools/sdk.py @@ -0,0 +1,8 @@ +import os + +from components.workflow import Workflow +from utils.handle_requests import RequestHandler + +class EasyDataSDK(): + def __init__(self, workflow_id=None, workflow_key=None): + self.workflow = Workflow(RequestHandler(os.environ["SDK_BASE_URL"], os.environ["SERVING_TOKEN"]), workflow_id=workflow_id, wf_key=workflow_key) diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py new file mode 100644 index 0000000000..8f92416b2f --- /dev/null +++ b/WorkflowExecAgent/tools/tools.py @@ -0,0 +1,17 @@ +from .sdk import EasyDataSDK + +def workflow_scheduler(params, workflow_id: int) -> dict: + sdk = EasyDataSDK(workflow_id=workflow_id) + + return sdk.workflow.start(params) + +def get_workflow_status(workflow_key: int): + sdk = EasyDataSDK(wf_key=workflow_key) + workflow_status = sdk.workflow.get_status()["workflow_status"] + + return workflow_status + +def get_workflow_data(workflow_key: int) -> str: + sdk = EasyDataSDK(wf_key=workflow_key) + + return sdk.workflow.result() diff --git a/WorkflowExecAgent/tools/tools.yaml b/WorkflowExecAgent/tools/tools.yaml new file mode 100644 index 0000000000..fa2a82f1f1 --- /dev/null +++ b/WorkflowExecAgent/tools/tools.yaml @@ -0,0 +1,29 @@ +workflow_scheduler: + description: "Used to start the workflow with a specified id" + callable_api: tools.py:workflow_scheduler + args_schema: + workflow_id: + type: int + description: Workflow id + params: + type: Dict[str, str] + description: Workflow paramaters. Dictionary keys can have whitespace + return_output: workflow_status + +workflow_status_checker: + description: "Used to check the execution status of the workflow." + callable_api: tools.py:workflow_status_checker + args_schema: + workflow_key: + type: int + description: Workflow key + return_output: workflow_status + +workflow_data_retriever: + description: "Used to retrieve workflow output data." + callable_api: tools.py:workflow_data_retriever + args_schema: + workflow_key: + type: int + description: Workflow key + return_output: workflow_output_data diff --git a/WorkflowExecAgent/tools/utils/handle_requests.py b/WorkflowExecAgent/tools/utils/handle_requests.py new file mode 100644 index 0000000000..4e4b68b020 --- /dev/null +++ b/WorkflowExecAgent/tools/utils/handle_requests.py @@ -0,0 +1,106 @@ +import requests +from functools import wraps + +from logger import get_logger +logger = get_logger(__name__) + +class RequestHandler(): + """ + Class for handling requests. + + Attributes: + base_url (string): The url of the API. + api_key (string): Secret token. + """ + + def __init__(self, base_url: str, api_key: str): + self.base_url = base_url + self.api_key = api_key + + def _make_request(self, endpoint, method='GET', data=None, stream=False): + url = f'{self.base_url}{endpoint}' + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}" + } + + error = "" + + if method == 'GET': + response = requests.get(url, headers=headers) + elif method == 'POST': + response = requests.post(url, data, headers=headers, stream=stream) + elif method == 'PUT': + response = requests.put(url, data, headers=headers) + elif method == 'DELETE': + response = requests.delete(url, headers=headers) + else: + raise ValueError(f"error: Invalid HTTP method {method}") + + @self._handle_request + def check_status(response): + response.raise_for_status() + + error = check_status(response) + + if error: + return error + + else: + try: + response.json() + return response.json() + except: + return response + + def _handle_request(self, func): + @wraps(func) + def decorated(response=None, *args, **kwargs): + if response is not None: + try: + logger.debug(response) + return func(response, *args, **kwargs) + + except requests.exceptions.HTTPError as errh: + error = {"error": f"{response.status_code} {response.reason} HTTP Error {errh}" } + except requests.exceptions.ConnectionError as errc: + error = {"error": f"{response.status_code} {response.reason} Connection Error {errc}" } + except requests.exceptions.Timeout as errt: + error = {"error": f"{response.status_code} {response.reason} Timeout Error {errt}" } + except requests.exceptions.ChunkedEncodingError as errck: + error = {"error": f"Invalid chunk encoding: {str(errck)}"} + except requests.exceptions.RequestException as err: + error = {"error": f"{response.status_code} {response.reason} {err}" } + except Exception as err: + logger.debug(response) + response = response.json() + logger.debug(response) + error_msg = f'{response["inner_code"]} {response["friendly_message"]}' + error = {"status_code": response["status_code"], "error": error_msg} + + return error + + else: + try: + return func(*args, **kwargs) + + except requests.exceptions.HTTPError as errh: + error = {"error": f"HTTP Error {errh}" } + except requests.exceptions.ConnectionError as errc: + error = {"error": f"Connection Error {errc}" } + except requests.exceptions.Timeout as errt: + error = {"error": f"Timeout Error {errt}" } + except requests.exceptions.ChunkedEncodingError as errck: + error = {"error": f"Invalid chunk encoding: {str(errck)}"} + except requests.exceptions.RequestException as err: + error = {"error": err } + + logger.error(f"{error}") + + return error + + return decorated + + def _handle_status_logger(self, val_status): + if val_status["status"] is False: + logger.error(val_status["msg"]) diff --git a/WorkflowExecAgent/tools/utils/logger.py b/WorkflowExecAgent/tools/utils/logger.py new file mode 100644 index 0000000000..74ee650bb3 --- /dev/null +++ b/WorkflowExecAgent/tools/utils/logger.py @@ -0,0 +1,13 @@ +import logging +import functools + +def get_logger(name=None): + logger = logging.getLogger(name) + + logger.setLevel(logging.INFO) + ch = logging.StreamHandler() + formatter = logging.Formatter('%(levelname)s - %(funcName)s - %(asctime)s - %(message)s') + ch.setFormatter(formatter) + logger.addHandler(ch) + + return logger From 87ed7db21e3decac49e30736d4424c875dcaea22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 05:25:02 +0000 Subject: [PATCH 02/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- WorkflowExecAgent/README.md | 8 +-- .../docker_compose/docker_compose.yaml | 3 + .../tools/components/component.py | 8 ++- .../tools/components/workflow.py | 55 +++++++++-------- WorkflowExecAgent/tools/sdk.py | 12 +++- WorkflowExecAgent/tools/tools.py | 8 ++- WorkflowExecAgent/tools/tools.yaml | 5 +- .../tools/utils/handle_requests.py | 61 ++++++++++--------- WorkflowExecAgent/tools/utils/logger.py | 8 ++- 9 files changed, 100 insertions(+), 68 deletions(-) diff --git a/WorkflowExecAgent/README.md b/WorkflowExecAgent/README.md index e8eb6b787e..df5a7d94d4 100644 --- a/WorkflowExecAgent/README.md +++ b/WorkflowExecAgent/README.md @@ -1,10 +1,10 @@ # Workflow Executor Agent -## +## ## Setup Guide -Workflow Executor will have a single docker image. +Workflow Executor will have a single docker image. Instructions to setup. @@ -25,11 +25,11 @@ export SDK_BASE_URL=${SDK_BASE_URL} export SERVING_TOKEN=${SERVING_TOKEN} export http_proxy=${http_proxy} export https_proxy=${https_proxy} -export llm_serving_url= +export llm_serving_url= ``` Launch service: ```sh docker compose -f compose.yaml up -d -``` \ No newline at end of file +``` diff --git a/WorkflowExecAgent/docker_compose/docker_compose.yaml b/WorkflowExecAgent/docker_compose/docker_compose.yaml index 02af9d45d0..64915d9601 100644 --- a/WorkflowExecAgent/docker_compose/docker_compose.yaml +++ b/WorkflowExecAgent/docker_compose/docker_compose.yaml @@ -1,3 +1,6 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + services: supervisor: image: opea/agent-langchain:latest diff --git a/WorkflowExecAgent/tools/components/component.py b/WorkflowExecAgent/tools/components/component.py index c90efd0f4a..e0491aaa19 100644 --- a/WorkflowExecAgent/tools/components/component.py +++ b/WorkflowExecAgent/tools/components/component.py @@ -1,6 +1,10 @@ -class Component(): +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +class Component: def __init__(self, request_handler): self.request_handler = request_handler - + def _make_request(self, *args, **kwargs): return self.request_handler._make_request(*args, **kwargs) diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py index 6ea78cfa8e..7a0b92bc94 100644 --- a/WorkflowExecAgent/tools/components/workflow.py +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -1,15 +1,18 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + import json from typing import Dict from .component import Component + class Workflow(Component): - """ - Class for handling EasyData workflow operations. + """Class for handling EasyData workflow operations. - Attributes: - workflow_id: workflow id - wf_key: workflow key. Generated and stored when starting a servable workflow. + Attributes: + workflow_id: workflow id + wf_key: workflow key. Generated and stored when starting a servable workflow. """ def __init__(self, request_handler, workflow_id=None, workflow_key=None): @@ -19,50 +22,50 @@ def __init__(self, request_handler, workflow_id=None, workflow_key=None): def start(self, params: Dict[str, str]) -> Dict[str, str]: """ - ``POST https://SDK_BASE_URL/serving/servable_workflows/{workflow_id}/start`` + ``POST https://SDK_BASE_URL/serving/servable_workflows/{workflow_id}/start`` - Starts a workflow with the workflow_id. + Starts a workflow with the workflow_id. - :param string workflow_id: Workflow id to start. + :param string workflow_id: Workflow id to start. - :returns: WorkflowKey + :returns: WorkflowKey - :rtype: dict + :rtype: dict """ data = json.dumps({"params": params}) - endpoint = f'serving/servable_workflows/{self.workflow_id}/start' + endpoint = f"serving/servable_workflows/{self.workflow_id}/start" wf_key = self._make_request(endpoint, "POST", data)["wf_key"] if wf_key: return {"message": f"Workflow successfully started. The workflow key is {wf_key}"} else: return {"message": "Workflow failed to start"} - + def get_status(self) -> Dict[str, str]: """ - ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/status`` - - Gets the workflow status. + ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/status`` - :param string workflow_key: Workflow id to retrieve status. + Gets the workflow status. - :returns: Status: Dictionary of presets + :param string workflow_key: Workflow id to retrieve status. - :rtype: json object + :returns: Status: Dictionary of presets + + :rtype: json object """ - - endpoint = f'serving/serving_workflows/{self.wf_key}/status' + + endpoint = f"serving/serving_workflows/{self.wf_key}/status" return self._make_request(endpoint, "GET") def result(self) -> list[Dict[str, str]]: """ - ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/results`` - - Gets the result. + ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/results`` + + Gets the result. - :returns: + :returns: - :rtype: json object + :rtype: json object """ - endpoint = f'serving/serving_workflows/{self.wf_key}/results' + endpoint = f"serving/serving_workflows/{self.wf_key}/results" return self._make_request(endpoint, "GET") diff --git a/WorkflowExecAgent/tools/sdk.py b/WorkflowExecAgent/tools/sdk.py index d1bc10651a..3afd45525a 100644 --- a/WorkflowExecAgent/tools/sdk.py +++ b/WorkflowExecAgent/tools/sdk.py @@ -1,8 +1,16 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + import os from components.workflow import Workflow from utils.handle_requests import RequestHandler -class EasyDataSDK(): + +class EasyDataSDK: def __init__(self, workflow_id=None, workflow_key=None): - self.workflow = Workflow(RequestHandler(os.environ["SDK_BASE_URL"], os.environ["SERVING_TOKEN"]), workflow_id=workflow_id, wf_key=workflow_key) + self.workflow = Workflow( + RequestHandler(os.environ["SDK_BASE_URL"], os.environ["SERVING_TOKEN"]), + workflow_id=workflow_id, + wf_key=workflow_key, + ) diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index 8f92416b2f..948b097c33 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -1,16 +1,22 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + from .sdk import EasyDataSDK + def workflow_scheduler(params, workflow_id: int) -> dict: sdk = EasyDataSDK(workflow_id=workflow_id) - + return sdk.workflow.start(params) + def get_workflow_status(workflow_key: int): sdk = EasyDataSDK(wf_key=workflow_key) workflow_status = sdk.workflow.get_status()["workflow_status"] return workflow_status + def get_workflow_data(workflow_key: int) -> str: sdk = EasyDataSDK(wf_key=workflow_key) diff --git a/WorkflowExecAgent/tools/tools.yaml b/WorkflowExecAgent/tools/tools.yaml index fa2a82f1f1..60985817c0 100644 --- a/WorkflowExecAgent/tools/tools.yaml +++ b/WorkflowExecAgent/tools/tools.yaml @@ -1,3 +1,6 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + workflow_scheduler: description: "Used to start the workflow with a specified id" callable_api: tools.py:workflow_scheduler @@ -7,7 +10,7 @@ workflow_scheduler: description: Workflow id params: type: Dict[str, str] - description: Workflow paramaters. Dictionary keys can have whitespace + description: Workflow parameters. Dictionary keys can have whitespace return_output: workflow_status workflow_status_checker: diff --git a/WorkflowExecAgent/tools/utils/handle_requests.py b/WorkflowExecAgent/tools/utils/handle_requests.py index 4e4b68b020..663a84f0bd 100644 --- a/WorkflowExecAgent/tools/utils/handle_requests.py +++ b/WorkflowExecAgent/tools/utils/handle_requests.py @@ -1,38 +1,39 @@ -import requests +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + from functools import wraps +import requests from logger import get_logger + logger = get_logger(__name__) -class RequestHandler(): - """ - Class for handling requests. - Attributes: - base_url (string): The url of the API. - api_key (string): Secret token. +class RequestHandler: + """Class for handling requests. + + Attributes: + base_url (string): The url of the API. + api_key (string): Secret token. """ def __init__(self, base_url: str, api_key: str): self.base_url = base_url self.api_key = api_key - def _make_request(self, endpoint, method='GET', data=None, stream=False): - url = f'{self.base_url}{endpoint}' - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.api_key}" - } - + def _make_request(self, endpoint, method="GET", data=None, stream=False): + url = f"{self.base_url}{endpoint}" + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}"} + error = "" - - if method == 'GET': + + if method == "GET": response = requests.get(url, headers=headers) - elif method == 'POST': + elif method == "POST": response = requests.post(url, data, headers=headers, stream=stream) - elif method == 'PUT': + elif method == "PUT": response = requests.put(url, data, headers=headers) - elif method == 'DELETE': + elif method == "DELETE": response = requests.delete(url, headers=headers) else: raise ValueError(f"error: Invalid HTTP method {method}") @@ -62,22 +63,22 @@ def decorated(response=None, *args, **kwargs): return func(response, *args, **kwargs) except requests.exceptions.HTTPError as errh: - error = {"error": f"{response.status_code} {response.reason} HTTP Error {errh}" } + error = {"error": f"{response.status_code} {response.reason} HTTP Error {errh}"} except requests.exceptions.ConnectionError as errc: - error = {"error": f"{response.status_code} {response.reason} Connection Error {errc}" } + error = {"error": f"{response.status_code} {response.reason} Connection Error {errc}"} except requests.exceptions.Timeout as errt: - error = {"error": f"{response.status_code} {response.reason} Timeout Error {errt}" } + error = {"error": f"{response.status_code} {response.reason} Timeout Error {errt}"} except requests.exceptions.ChunkedEncodingError as errck: error = {"error": f"Invalid chunk encoding: {str(errck)}"} except requests.exceptions.RequestException as err: - error = {"error": f"{response.status_code} {response.reason} {err}" } + error = {"error": f"{response.status_code} {response.reason} {err}"} except Exception as err: logger.debug(response) response = response.json() logger.debug(response) error_msg = f'{response["inner_code"]} {response["friendly_message"]}' error = {"status_code": response["status_code"], "error": error_msg} - + return error else: @@ -85,22 +86,22 @@ def decorated(response=None, *args, **kwargs): return func(*args, **kwargs) except requests.exceptions.HTTPError as errh: - error = {"error": f"HTTP Error {errh}" } + error = {"error": f"HTTP Error {errh}"} except requests.exceptions.ConnectionError as errc: - error = {"error": f"Connection Error {errc}" } + error = {"error": f"Connection Error {errc}"} except requests.exceptions.Timeout as errt: - error = {"error": f"Timeout Error {errt}" } + error = {"error": f"Timeout Error {errt}"} except requests.exceptions.ChunkedEncodingError as errck: error = {"error": f"Invalid chunk encoding: {str(errck)}"} except requests.exceptions.RequestException as err: - error = {"error": err } + error = {"error": err} logger.error(f"{error}") return error - + return decorated - + def _handle_status_logger(self, val_status): if val_status["status"] is False: logger.error(val_status["msg"]) diff --git a/WorkflowExecAgent/tools/utils/logger.py b/WorkflowExecAgent/tools/utils/logger.py index 74ee650bb3..783bd57728 100644 --- a/WorkflowExecAgent/tools/utils/logger.py +++ b/WorkflowExecAgent/tools/utils/logger.py @@ -1,12 +1,16 @@ -import logging +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + import functools +import logging + def get_logger(name=None): logger = logging.getLogger(name) logger.setLevel(logging.INFO) ch = logging.StreamHandler() - formatter = logging.Formatter('%(levelname)s - %(funcName)s - %(asctime)s - %(message)s') + formatter = logging.Formatter("%(levelname)s - %(funcName)s - %(asctime)s - %(message)s") ch.setFormatter(formatter) logger.addHandler(ch) From bc5cc124ecd89fff559e0e5968c7560fab66725a Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Tue, 8 Oct 2024 09:53:27 +0000 Subject: [PATCH 03/21] Update workflow executor example Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/README.md | 7 ++-- .../docker_compose/docker_compose.yaml | 6 +-- .../tools/components/workflow.py | 7 ++-- WorkflowExecAgent/tools/tools.py | 7 +--- .../tools/utils/handle_requests.py | 38 ++----------------- WorkflowExecAgent/tools/utils/logger.py | 17 --------- 6 files changed, 14 insertions(+), 68 deletions(-) delete mode 100644 WorkflowExecAgent/tools/utils/logger.py diff --git a/WorkflowExecAgent/README.md b/WorkflowExecAgent/README.md index df5a7d94d4..9b7bb20933 100644 --- a/WorkflowExecAgent/README.md +++ b/WorkflowExecAgent/README.md @@ -1,7 +1,5 @@ # Workflow Executor Agent -## - ## Setup Guide Workflow Executor will have a single docker image. @@ -19,13 +17,14 @@ Configure .env file with the following. Replace the variables according to your ```sh export ip_address=$(hostname -I | awk '{print $1}') export SERVING_PORT=8000 -export LLM_MODEL="mistralai/Mistral-7B-Instruct-v0.3" +export model="mistralai/Mistral-7B-Instruct-v0.3" export HUGGINGFACEHUB_API_TOKEN=${HF_TOKEN} export SDK_BASE_URL=${SDK_BASE_URL} export SERVING_TOKEN=${SERVING_TOKEN} export http_proxy=${http_proxy} export https_proxy=${https_proxy} -export llm_serving_url= +export recursion_limit=${recursion_limit} +export OPENAI_API_KEY=${OPENAI_API_KEY} ``` Launch service: diff --git a/WorkflowExecAgent/docker_compose/docker_compose.yaml b/WorkflowExecAgent/docker_compose/docker_compose.yaml index 64915d9601..780af5bda0 100644 --- a/WorkflowExecAgent/docker_compose/docker_compose.yaml +++ b/WorkflowExecAgent/docker_compose/docker_compose.yaml @@ -2,9 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 services: - supervisor: + worflowexec-agent: image: opea/agent-langchain:latest - container_name: supervisor-agent-endpoint + container_name: worflowexec-agent-endpoint volumes: - ${WORKDIR}/GenAIComps/comps/agent/langchain/:/home/user/comps/agent/langchain/ - ${TOOLSET_PATH}:/home/user/tools/ @@ -14,7 +14,7 @@ services: environment: ip_address: ${ip_address} strategy: workflow_executor - recursion_limit: ${recursion_limit_supervisor} + recursion_limit: ${recursion_limit} llm_engine: openai OPENAI_API_KEY: ${OPENAI_API_KEY} model: ${model} diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py index 7a0b92bc94..3091b26738 100644 --- a/WorkflowExecAgent/tools/components/workflow.py +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -6,7 +6,6 @@ from .component import Component - class Workflow(Component): """Class for handling EasyData workflow operations. @@ -36,10 +35,10 @@ def start(self, params: Dict[str, str]) -> Dict[str, str]: endpoint = f"serving/servable_workflows/{self.workflow_id}/start" wf_key = self._make_request(endpoint, "POST", data)["wf_key"] if wf_key: - return {"message": f"Workflow successfully started. The workflow key is {wf_key}"} + return f"Workflow successfully started. The workflow key is {wf_key}." else: - return {"message": "Workflow failed to start"} - + return "Workflow failed to start" + def get_status(self) -> Dict[str, str]: """ ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/status`` diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index 948b097c33..2c32c28a32 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -3,21 +3,18 @@ from .sdk import EasyDataSDK - def workflow_scheduler(params, workflow_id: int) -> dict: sdk = EasyDataSDK(workflow_id=workflow_id) return sdk.workflow.start(params) - -def get_workflow_status(workflow_key: int): +def workflow_status_checker(workflow_key: int): sdk = EasyDataSDK(wf_key=workflow_key) workflow_status = sdk.workflow.get_status()["workflow_status"] return workflow_status - -def get_workflow_data(workflow_key: int) -> str: +def workflow_data_retriever(workflow_key: int) -> str: sdk = EasyDataSDK(wf_key=workflow_key) return sdk.workflow.result() diff --git a/WorkflowExecAgent/tools/utils/handle_requests.py b/WorkflowExecAgent/tools/utils/handle_requests.py index 663a84f0bd..ea8fc777f7 100644 --- a/WorkflowExecAgent/tools/utils/handle_requests.py +++ b/WorkflowExecAgent/tools/utils/handle_requests.py @@ -2,12 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from functools import wraps - import requests -from logger import get_logger - -logger = get_logger(__name__) - class RequestHandler: """Class for handling requests. @@ -59,7 +54,7 @@ def _handle_request(self, func): def decorated(response=None, *args, **kwargs): if response is not None: try: - logger.debug(response) + print(response) return func(response, *args, **kwargs) except requests.exceptions.HTTPError as errh: @@ -73,35 +68,8 @@ def decorated(response=None, *args, **kwargs): except requests.exceptions.RequestException as err: error = {"error": f"{response.status_code} {response.reason} {err}"} except Exception as err: - logger.debug(response) - response = response.json() - logger.debug(response) - error_msg = f'{response["inner_code"]} {response["friendly_message"]}' - error = {"status_code": response["status_code"], "error": error_msg} - - return error - - else: - try: - return func(*args, **kwargs) - - except requests.exceptions.HTTPError as errh: - error = {"error": f"HTTP Error {errh}"} - except requests.exceptions.ConnectionError as errc: - error = {"error": f"Connection Error {errc}"} - except requests.exceptions.Timeout as errt: - error = {"error": f"Timeout Error {errt}"} - except requests.exceptions.ChunkedEncodingError as errck: - error = {"error": f"Invalid chunk encoding: {str(errck)}"} - except requests.exceptions.RequestException as err: - error = {"error": err} - - logger.error(f"{error}") - + error = err + return error return decorated - - def _handle_status_logger(self, val_status): - if val_status["status"] is False: - logger.error(val_status["msg"]) diff --git a/WorkflowExecAgent/tools/utils/logger.py b/WorkflowExecAgent/tools/utils/logger.py deleted file mode 100644 index 783bd57728..0000000000 --- a/WorkflowExecAgent/tools/utils/logger.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -import functools -import logging - - -def get_logger(name=None): - logger = logging.getLogger(name) - - logger.setLevel(logging.INFO) - ch = logging.StreamHandler() - formatter = logging.Formatter("%(levelname)s - %(funcName)s - %(asctime)s - %(message)s") - ch.setFormatter(formatter) - logger.addHandler(ch) - - return logger From 03f0d0165f398e80e6ffb37bc1b5cacb2cb50838 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:08:05 +0000 Subject: [PATCH 04/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- WorkflowExecAgent/tools/components/workflow.py | 3 ++- WorkflowExecAgent/tools/tools.py | 3 +++ WorkflowExecAgent/tools/utils/handle_requests.py | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py index 3091b26738..dfa076ba91 100644 --- a/WorkflowExecAgent/tools/components/workflow.py +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -6,6 +6,7 @@ from .component import Component + class Workflow(Component): """Class for handling EasyData workflow operations. @@ -38,7 +39,7 @@ def start(self, params: Dict[str, str]) -> Dict[str, str]: return f"Workflow successfully started. The workflow key is {wf_key}." else: return "Workflow failed to start" - + def get_status(self) -> Dict[str, str]: """ ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/status`` diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index 2c32c28a32..fa73ea1b0e 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -3,17 +3,20 @@ from .sdk import EasyDataSDK + def workflow_scheduler(params, workflow_id: int) -> dict: sdk = EasyDataSDK(workflow_id=workflow_id) return sdk.workflow.start(params) + def workflow_status_checker(workflow_key: int): sdk = EasyDataSDK(wf_key=workflow_key) workflow_status = sdk.workflow.get_status()["workflow_status"] return workflow_status + def workflow_data_retriever(workflow_key: int) -> str: sdk = EasyDataSDK(wf_key=workflow_key) diff --git a/WorkflowExecAgent/tools/utils/handle_requests.py b/WorkflowExecAgent/tools/utils/handle_requests.py index ea8fc777f7..caa5c8bde8 100644 --- a/WorkflowExecAgent/tools/utils/handle_requests.py +++ b/WorkflowExecAgent/tools/utils/handle_requests.py @@ -2,8 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 from functools import wraps + import requests + class RequestHandler: """Class for handling requests. @@ -69,7 +71,7 @@ def decorated(response=None, *args, **kwargs): error = {"error": f"{response.status_code} {response.reason} {err}"} except Exception as err: error = err - + return error return decorated From 11344a6638493cc45030540ce0600e952cb57fc7 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Wed, 16 Oct 2024 07:49:52 +0000 Subject: [PATCH 05/21] Update workflow executor example * Use single tool implementation * Add CICD validation script * Update example readme Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/README.md | 127 ++++++++++++++++-- .../docker_compose/docker_compose.yaml | 8 +- .../docker_image_build/build.yaml | 13 ++ WorkflowExecAgent/tests/1_build_images.sh | 29 ++++ .../tests/2_start_vllm_service.sh | 66 +++++++++ .../tests/3_launch_and_validate_agent.sh | 65 +++++++++ WorkflowExecAgent/tests/README.md | 38 ++++++ WorkflowExecAgent/tests/test_compose.sh | 30 +++++ .../tool_chat_template_mistral_custom.jinja | 83 ++++++++++++ .../tools/components/workflow.py | 28 ++-- WorkflowExecAgent/tools/sdk.py | 16 ++- WorkflowExecAgent/tools/tools.py | 56 +++++--- WorkflowExecAgent/tools/tools.yaml | 30 +---- .../tools/utils/handle_requests.py | 1 - 14 files changed, 506 insertions(+), 84 deletions(-) create mode 100644 WorkflowExecAgent/docker_image_build/build.yaml create mode 100644 WorkflowExecAgent/tests/1_build_images.sh create mode 100644 WorkflowExecAgent/tests/2_start_vllm_service.sh create mode 100644 WorkflowExecAgent/tests/3_launch_and_validate_agent.sh create mode 100644 WorkflowExecAgent/tests/README.md create mode 100644 WorkflowExecAgent/tests/test_compose.sh create mode 100644 WorkflowExecAgent/tests/tool_chat_template_mistral_custom.jinja diff --git a/WorkflowExecAgent/README.md b/WorkflowExecAgent/README.md index 9b7bb20933..67c4896e02 100644 --- a/WorkflowExecAgent/README.md +++ b/WorkflowExecAgent/README.md @@ -1,34 +1,135 @@ # Workflow Executor Agent -## Setup Guide +## Overview -Workflow Executor will have a single docker image. +GenAI Workflow Executor Example showcases the capability to handle data/AI workflow operations via LangChain agents to execute custom-defined workflow-based tools. These workflow tools can be interfaced from any 3rd-party tools in the market (no-code/low-code/IDE) such as Alteryx, RapidMiner, Power BI, Intel Data Insight Automation which allows users to create complex data/AI workflow operations for different use-cases. -Instructions to setup. +### Workflow Executor + +This example demonstrates a single React-LangGraph with a `Workflow Executor` tool to ingest a user prompt to execute workflows and return an agent reasoning response based on the workflow output data. + +First the LLM extracts the relevant information from the user query based on the schema of the tool in `tools/tools.yaml`. Then the agent sends this `AgentState` to the `Workflow Executor` tool. + +`Workflow Executor` tool uses `EasyDataSDK` class as seen under `tools/sdk.py` to interface with several high-level API's. There are 3 steps to this tool implementation: + +1. Starts the workflow with workflow parameters and workflow id extracted from the user query. + +2. Periodically checks the workflow status for completion or failure. This may be through a database which stores the current status of the workflow + +3. Retrieves the output data from the workflow through a storage service. + +The `AgentState` is sent back to the LLM for reasoning. Based on the output data, the LLM generates a response to answer the user's input prompt. + +Below shows an illustration of this flow: + +![image](https://github.com/user-attachments/assets/cb135042-1505-4aef-8822-c78c2f72aa2a) + +### Workflow Serving for Agent + +As an example, here we have a Churn Prediction use-case workflow as the serving workflow for the agent execution. It is created through Intel Data Insight Automation platform. The image below shows a snapshot of the Churn Prediction workflow. + +![image](https://github.com/user-attachments/assets/c067f8b3-86cf-4abc-a8bd-51a98de8172d) + +The workflow contains 2 paths which can be seen in the workflow illustrated, the top path and bottom path. + +1. Top path - The training path which ends at the random forest classifier node is the training path. The data is cleaned through a series of nodes and used to train a random forest model for prediction. + +2. Bottom path - The inference path where trained random forest model is used for inferencing based on input parameter. + +For this agent workflow execution, the inferencing path is executed to yield the final output result of the `Model Predictor` node. The same output is returned to the `Workflow Executor` tool through the `Langchain API Serving` node. + +There are `Serving Parameters` in the workflow, which are the tool input variables used to start a workflow instance obtained from `params` the LLM extracts from the user query. Below shows the parameter configuration option for the Intel Data Insight Automation workflow UI. + +![image](https://github.com/user-attachments/assets/ce8ef01a-56ff-4278-b84d-b6e4592b28c6) + +Manually running the workflow yields the tabular data output as shown below: + +![image](https://github.com/user-attachments/assets/241c1aba-2a24-48da-8005-ec7bfe657179) + +In the workflow serving for agent, this output will be returned to the `Workflow Executor` tool. The LLM can then answer the user's original question based on this output. + +To start prompting the agent microservice, we will use the following command for this use case: + +```sh +curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ + "query": "I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." + }' +``` + +The user has to provide a `workflow_id` and workflow `params` in the query. `workflow_id` a unique id used for serving the workflow to the microservice. Notice that the `query` string includes all the workflow `params` which the user has defined in the workflow. The LLM will extract these parameters into a dictionary format for the workflow `Serving Parameters` as shown below: + +```python +params = { + "gender": "Female", + "tenure": 55, + "MonthlyAvgCharges": 103.7 +} +``` + +These parameters will be passed into the `Workflow Executor` tool to start the workflow execution of specified `workflow_id`. Thus, everything will be handled via the microservice. + +And finally here are the results from the microservice logs: + +![image](https://github.com/user-attachments/assets/969fefb7-543d-427f-a56c-dc70e474ae60) + +## Microservice Setup + +### Start Agent Microservice + +Workflow Executor will have a single docker image. First, build the agent docker image. ```sh git clone https://github.com/opea-project/GenAIExamples.git -cd GenAIExamples/WorkflowExecutor/ -docker build -t opea/workflow-executor:latest -f Dockerfile . +cd GenAIExamples//WorkflowExecAgent/docker_image_build/ +docker compose -f build.yaml build --no-cache ``` -Configure .env file with the following. Replace the variables according to your usecase. +Configure `GenAIExamples/WorkflowExecAgent/docker_compose/.env` file with the following. Replace the variables according to your usecase. ```sh -export ip_address=$(hostname -I | awk '{print $1}') -export SERVING_PORT=8000 -export model="mistralai/Mistral-7B-Instruct-v0.3" -export HUGGINGFACEHUB_API_TOKEN=${HF_TOKEN} export SDK_BASE_URL=${SDK_BASE_URL} export SERVING_TOKEN=${SERVING_TOKEN} +export HUGGINGFACEHUB_API_TOKEN=${HF_TOKEN} +export llm_engine=${llm_engine} +export llm_endpoint_url=${llm_endpoint_url} +export ip_address=$(hostname -I | awk '{print $1}') +export model="mistralai/Mistral-7B-Instruct-v0.3" +export recursion_limit=${recursion_limit} +export temperature=0 +export max_new_tokens=1000 +export WORKDIR=${WORKDIR} +export TOOLSET_PATH=$WORKDIR/GenAIExamples/WorkflowExecAgent/tools/ export http_proxy=${http_proxy} export https_proxy=${https_proxy} -export recursion_limit=${recursion_limit} -export OPENAI_API_KEY=${OPENAI_API_KEY} ``` -Launch service: +Launch service by running the docker compose command. ```sh +cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_compose docker compose -f compose.yaml up -d ``` + +### Validate service + +The microservice logs can be viewed using: + +```sh +docker logs workflowexec-agent-endpoint +``` + +You should be able to see "HTTP server setup successful" upon successful startup. + +You can validate the service using the following command: + +```sh +curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ + "query": "I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." + }' +``` + +Update the `query` with the workflow parameters, workflow id, etc based on the workflow context. + +## Roadmap + +Phase II: Agent memory integration to enable capability to store tool intermediate results, such as workflow instance key. diff --git a/WorkflowExecAgent/docker_compose/docker_compose.yaml b/WorkflowExecAgent/docker_compose/docker_compose.yaml index 780af5bda0..913dd03210 100644 --- a/WorkflowExecAgent/docker_compose/docker_compose.yaml +++ b/WorkflowExecAgent/docker_compose/docker_compose.yaml @@ -4,7 +4,7 @@ services: worflowexec-agent: image: opea/agent-langchain:latest - container_name: worflowexec-agent-endpoint + container_name: workflowexec-agent-endpoint volumes: - ${WORKDIR}/GenAIComps/comps/agent/langchain/:/home/user/comps/agent/langchain/ - ${TOOLSET_PATH}:/home/user/tools/ @@ -13,10 +13,10 @@ services: ipc: host environment: ip_address: ${ip_address} - strategy: workflow_executor + strategy: react_langgraph recursion_limit: ${recursion_limit} - llm_engine: openai - OPENAI_API_KEY: ${OPENAI_API_KEY} + llm_engine: ${llm_engine} + llm_endpoint_url: ${llm_endpoint_url} model: ${model} temperature: ${temperature} max_new_tokens: ${max_new_tokens} diff --git a/WorkflowExecAgent/docker_image_build/build.yaml b/WorkflowExecAgent/docker_image_build/build.yaml new file mode 100644 index 0000000000..e0c8a75b06 --- /dev/null +++ b/WorkflowExecAgent/docker_image_build/build.yaml @@ -0,0 +1,13 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +services: + agent-langchain: + build: + context: GenAIComps + dockerfile: comps/agent/langchain/Dockerfile + args: + http_proxy: ${http_proxy} + https_proxy: ${https_proxy} + no_proxy: ${no_proxy} + image: ${REGISTRY:-opea}/agent-langchain:${TAG:-latest} \ No newline at end of file diff --git a/WorkflowExecAgent/tests/1_build_images.sh b/WorkflowExecAgent/tests/1_build_images.sh new file mode 100644 index 0000000000..96af4139bd --- /dev/null +++ b/WorkflowExecAgent/tests/1_build_images.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e +WORKPATH=$(dirname "$PWD") +export WORKDIR=$WORKPATH/../../ +echo "WORKDIR=${WORKDIR}" + +function get_genai_comps() { + if [ ! -d "GenAIComps" ] ; then + git clone https://github.com/opea-project/GenAIComps.git && cd GenAIComps && git checkout "${opea_branch:-"main"}" && cd ../ + fi +} + +function build_agent_docker_image() { + cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_image_build/ + get_genai_comps + echo "Build agent image with --no-cache..." + docker compose -f build.yaml build --no-cache +} + +function main() { + echo "==================== Build agent docker image ====================" + build_agent_docker_image + echo "==================== Build agent docker image completed ====================" +} + +main \ No newline at end of file diff --git a/WorkflowExecAgent/tests/2_start_vllm_service.sh b/WorkflowExecAgent/tests/2_start_vllm_service.sh new file mode 100644 index 0000000000..8aa0c687b0 --- /dev/null +++ b/WorkflowExecAgent/tests/2_start_vllm_service.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +WORKPATH=$(dirname "$PWD") +LOG_PATH="$WORKPATH/tests" +vllm_port=${vllm_port} +model=mistralai/Mistral-7B-Instruct-v0.3 +export WORKDIR=$WORKPATH/../../ +export HF_TOKEN=${HF_TOKEN} +export VLLM_CPU_OMP_THREADS_BIND=${VLLM_CPU_OMP_THREADS_BIND} + +function build_vllm_docker_image() { + echo "Building the vllm docker images" + cd $WORKPATH + echo $WORKPATH + if [ ! -d "./vllm" ]; then + git clone https://github.com/vllm-project/vllm.git + cd ./vllm; git checkout tags/v0.6.0 + fi + cd ./vllm + docker build -f Dockerfile.cpu -t vllm-cpu-env --shm-size=100g . + if [ $? -ne 0 ]; then + echo "opea/vllm:cpu failed" + exit 1 + else + echo "opea/vllm:cpu successful" + fi +} + +function start_vllm_service() { + echo "start vllm service" + docker run -d -p ${vllm_port}:8083 --rm --network=host --name test-comps-vllm-service -v ~/.cache/huggingface:/root/.cache/huggingface -v ${WORKPATH}/tests/tool_chat_template_mistral_custom.jinja:/root/tool_chat_template_mistral_custom.jinja -e HF_TOKEN=$HF_TOKEN -e http_proxy=$http_proxy -e https_proxy=$https_proxy -it vllm-cpu-env --model ${model} --port 8083 --chat-template /root/tool_chat_template_mistral_custom.jinja --enable-auto-tool-choice --tool-call-parser mistral + echo ${LOG_PATH}/vllm-service.log + sleep 5s + echo "Waiting vllm ready" + n=0 + until [[ "$n" -ge 100 ]] || [[ $ready == true ]]; do + docker logs test-comps-vllm-service &> ${LOG_PATH}/vllm-service.log + n=$((n+1)) + if grep -q "Uvicorn running on" ${LOG_PATH}/vllm-service.log; then + break + fi + if grep -q "No such container" ${LOG_PATH}/vllm-service.log; then + echo "container test-comps-vllm-service not found" + exit 1 + fi + sleep 5s + done + sleep 5s + echo "Service started successfully" +} + +function main() { + echo "==================== Build vllm docker image ====================" + build_vllm_docker_image + echo "==================== Build vllm docker image completed ====================" + + echo "==================== Start vllm docker service ====================" + start_vllm_service + echo "==================== Start vllm docker service completed ====================" +} + +main \ No newline at end of file diff --git a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh new file mode 100644 index 0000000000..f826ad1ff0 --- /dev/null +++ b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +WORKPATH=$(dirname "$PWD") +workflow_id=${workflow_id} +vllm_port=${vllm_port} +export WORKDIR=$WORKPATH/../../ +echo "WORKDIR=${WORKDIR}" +export SDK_BASE_URL=${SDK_BASE_URL} +export SERVING_TOKEN=${SERVING_TOKEN} +export HF_TOKEN=${HF_TOKEN} +export llm_engine=vllm +export ip_address=$(hostname -I | awk '{print $1}') +export llm_endpoint_url=${ip_address}:${vllm_port} +export model=mistralai/Mistral-7B-Instruct-v0.3 +export recursion_limit=25 +export temperature=0 +export max_new_tokens=1000 +export TOOLSET_PATH=$WORKDIR/GenAIExamples/WorkflowExecAgent/tools/ + +function start_agent_and_api_server() { + echo "Starting Agent services" + cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_compose + WORKDIR=$WORKPATH/docker_image_build/ docker compose -f docker_compose.yaml up -d + echo "Waiting agent service ready" + sleep 5s +} + +function validate() { + local CONTENT="$1" + local EXPECTED_RESULT="$2" + local SERVICE_NAME="$3" + + if echo "$CONTENT" | grep -q "$EXPECTED_RESULT"; then + echo "[ $SERVICE_NAME ] Content is as expected: $CONTENT" + echo "[TEST INFO]: Workflow Executor agent service PASSED" + else + echo "[ $SERVICE_NAME ] Content does not match the expected result: $CONTENT" + echo "[TEST INFO]: Workflow Executor agent service FAILED" + fi +} + +function validate_agent_service() { + echo "----------------Test agent ----------------" + local CONTENT=$(curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ + "query": "I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." + }') + validate "$CONTENT" "The entry is not likely to churn" "workflowexec-agent-endpoint" + docker logs workflowexec-agent-endpoint +} + +function main() { + echo "==================== Start agent ====================" + start_agent_and_api_server + echo "==================== Agent started ====================" + + echo "==================== Validate agent service ====================" + validate_agent_service + echo "==================== Agent service validated ====================" +} + +main \ No newline at end of file diff --git a/WorkflowExecAgent/tests/README.md b/WorkflowExecAgent/tests/README.md new file mode 100644 index 0000000000..0886880006 --- /dev/null +++ b/WorkflowExecAgent/tests/README.md @@ -0,0 +1,38 @@ +# Validate Workflow Agent Microservice + +Microservice validation for Intel Data Insight Automation platform workflow serving. + +## Usage + +Configure necessary variables as listed below. Replace the variables according to your usecase. + +```sh +export SDK_BASE_URL=${SDK_BASE_URL} +export SERVING_TOKEN=${SERVING_TOKEN} +export HUGGINGFACEHUB_API_TOKEN=${HF_TOKEN} +export workflow_id=${workflow_id} # workflow_id of the serving workflow +export vllm_port=${vllm_port} # vllm serving port +export ip_address=$(hostname -I | awk '{print $1}') +export VLLM_CPU_OMP_THREADS_BIND=${VLLM_CPU_OMP_THREADS_BIND} +export http_proxy=${http_proxy} +export https_proxy=${https_proxy} +``` + +Note: `SDK_BASE_URL` and `SERVING_TOKEN` can be obtained from Intel Data Insight Automation platform. + +Launch validation by running the following command. + +```sh +cd GenAIExamples/WorkflowExecAgent/tests +. /test_compose.sh +``` + +`test_compose.sh` will run the other `.sh` files under `tests/`. The validation script launches 1 docker container for the agent microservice, and another for the vllm model serving on CPU. When validation is completed, all containers will be stopped. + +The validation is tested by checking if the model reasoning output response matches a partial substring. The expected output is shown below: + +![image](https://github.com/user-attachments/assets/88081bc8-7b73-470d-970e-92e0fe5f96ec) + +## Note + +- Currently the validation test is only designed with vllm model serving (CPU only). diff --git a/WorkflowExecAgent/tests/test_compose.sh b/WorkflowExecAgent/tests/test_compose.sh new file mode 100644 index 0000000000..14d7c711ad --- /dev/null +++ b/WorkflowExecAgent/tests/test_compose.sh @@ -0,0 +1,30 @@ +function stop_agent_and_api_server() { + echo "Stopping Agent services" + docker rm --force $(docker ps -a -q --filter="name=workflowexec-agent-endpoint") +} + +function stop_vllm_docker() { + cid=$(docker ps -aq --filter "name=test-comps-vllm-service") + echo "Stopping the docker containers "${cid} + if [[ ! -z "$cid" ]]; then docker rm $cid -f && sleep 1s; fi + echo "Docker containers stopped successfully" +} + +echo "=================== #1 Building docker images ====================" +bash 1_build_images.sh +echo "=================== #1 Building docker images completed ====================" + +echo "=================== #2 Start vllm service ====================" +bash 2_start_vllm_service.sh +echo "=================== #2 Start vllm service completed ====================" + +echo "=================== #3 Start agent and API server ====================" +bash 3_launch_and_validate_agent.sh +echo "=================== #3 Agent test completed ====================" + +echo "=================== #4 Stop agent and API server ====================" +stop_agent_and_api_server +stop_vllm_docker +echo "=================== #4 Agent and API server stopped ====================" + +echo "ALL DONE!" \ No newline at end of file diff --git a/WorkflowExecAgent/tests/tool_chat_template_mistral_custom.jinja b/WorkflowExecAgent/tests/tool_chat_template_mistral_custom.jinja new file mode 100644 index 0000000000..05905ea356 --- /dev/null +++ b/WorkflowExecAgent/tests/tool_chat_template_mistral_custom.jinja @@ -0,0 +1,83 @@ +{%- if messages[0]["role"] == "system" %} + {%- set system_message = messages[0]["content"] %} + {%- set loop_messages = messages[1:] %} +{%- else %} + {%- set loop_messages = messages %} +{%- endif %} +{%- if not tools is defined %} + {%- set tools = none %} +{%- endif %} +{%- set user_messages = loop_messages | selectattr("role", "equalto", "user") | list %} + +{%- for message in loop_messages | rejectattr("role", "equalto", "tool") | rejectattr("role", "equalto", "tool_results") | selectattr("tool_calls", "undefined") %} +{%- endfor %} + +{{- bos_token }} +{%- for message in loop_messages %} + {%- if message["role"] == "user" %} + {%- if tools is not none and (message == user_messages[-1]) %} + {{- "[AVAILABLE_TOOLS] [" }} + {%- for tool in tools %} + {%- set tool = tool.function %} + {{- '{"type": "function", "function": {' }} + {%- for key, val in tool.items() if key != "return" %} + {%- if val is string %} + {{- '"' + key + '": "' + val + '"' }} + {%- else %} + {{- '"' + key + '": ' + val|tojson }} + {%- endif %} + {%- if not loop.last %} + {{- ", " }} + {%- endif %} + {%- endfor %} + {{- "}}" }} + {%- if not loop.last %} + {{- ", " }} + {%- else %} + {{- "]" }} + {%- endif %} + {%- endfor %} + {{- "[/AVAILABLE_TOOLS]" }} + {%- endif %} + {%- if loop.last and system_message is defined %} + {{- "[INST] " + system_message + "\n\n" + message["content"] + "[/INST]" }} + {%- else %} + {{- "[INST] " + message["content"] + "[/INST]" }} + {%- endif %} + {%- elif message["role"] == "tool_calls" or message.tool_calls is defined %} + {%- if message.tool_calls is defined %} + {%- set tool_calls = message.tool_calls %} + {%- else %} + {%- set tool_calls = message.content %} + {%- endif %} + {{- "[TOOL_CALLS] [" }} + {%- for tool_call in tool_calls %} + {%- set out = tool_call.function|tojson %} + {{- out[:-1] }} + {%- if not tool_call.id is defined or tool_call.id|length < 9 %} + {{- raise_exception("Tool call IDs should be alphanumeric strings with length >= 9! (1)" + tool_call.id) }} + {%- endif %} + {{- ', "id": "' + tool_call.id[-9:] + '"}' }} + {%- if not loop.last %} + {{- ", " }} + {%- else %} + {{- "]" + eos_token }} + {%- endif %} + {%- endfor %} + {%- elif message["role"] == "assistant" %} + {{- " " + message["content"] + eos_token }} + {%- elif message["role"] == "tool_results" or message["role"] == "tool" %} + {%- if message.content is defined and message.content.content is defined %} + {%- set content = message.content.content %} + {%- else %} + {%- set content = message.content %} + {%- endif %} + {{- '[TOOL_RESULTS] {"content": ' + content|string + ", " }} + {%- if not message.tool_call_id is defined or message.tool_call_id|length < 9 %} + {{- raise_exception("Tool call IDs should be alphanumeric strings with length >= 9! (2)" + message.tool_call_id) }} + {%- endif %} + {{- '"call_id": "' + message.tool_call_id[-9:] + '"}[/TOOL_RESULTS]' }} + {%- else %} + {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }} + {%- endif %} +{%- endfor %} diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py index dfa076ba91..48f4e7d432 100644 --- a/WorkflowExecAgent/tools/components/workflow.py +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -4,8 +4,7 @@ import json from typing import Dict -from .component import Component - +from tools.components.component import Component class Workflow(Component): """Class for handling EasyData workflow operations. @@ -24,19 +23,20 @@ def start(self, params: Dict[str, str]) -> Dict[str, str]: """ ``POST https://SDK_BASE_URL/serving/servable_workflows/{workflow_id}/start`` - Starts a workflow with the workflow_id. + Starts a workflow instance with the workflow_id and parameters provided. + Returns a workflow key used to track the workflow instance. - :param string workflow_id: Workflow id to start. + :param dict params: Workflow parameters used to start workflow. :returns: WorkflowKey - :rtype: dict + :rtype: string """ data = json.dumps({"params": params}) endpoint = f"serving/servable_workflows/{self.workflow_id}/start" - wf_key = self._make_request(endpoint, "POST", data)["wf_key"] - if wf_key: - return f"Workflow successfully started. The workflow key is {wf_key}." + self.wf_key = self._make_request(endpoint, "POST", data)["wf_key"] + if self.wf_key: + return f"Workflow successfully started. The workflow key is {self.wf_key}." else: return "Workflow failed to start" @@ -46,11 +46,9 @@ def get_status(self) -> Dict[str, str]: Gets the workflow status. - :param string workflow_key: Workflow id to retrieve status. - - :returns: Status: Dictionary of presets + :returns: WorkflowStatus - :rtype: json object + :rtype: string """ endpoint = f"serving/serving_workflows/{self.wf_key}/status" @@ -60,11 +58,11 @@ def result(self) -> list[Dict[str, str]]: """ ``GET https://SDK_BASE_URL/serving/serving_workflows/{workflow_key}/results`` - Gets the result. + Gets the workflow output result. - :returns: + :returns: WorkflowOutputData - :rtype: json object + :rtype: string """ endpoint = f"serving/serving_workflows/{self.wf_key}/results" diff --git a/WorkflowExecAgent/tools/sdk.py b/WorkflowExecAgent/tools/sdk.py index 3afd45525a..81349e5708 100644 --- a/WorkflowExecAgent/tools/sdk.py +++ b/WorkflowExecAgent/tools/sdk.py @@ -3,14 +3,16 @@ import os -from components.workflow import Workflow -from utils.handle_requests import RequestHandler - +from tools.components.workflow import Workflow +from tools.utils.handle_requests import RequestHandler class EasyDataSDK: - def __init__(self, workflow_id=None, workflow_key=None): - self.workflow = Workflow( - RequestHandler(os.environ["SDK_BASE_URL"], os.environ["SERVING_TOKEN"]), + def __init__(self): + self.request_handler = RequestHandler(os.environ["SDK_BASE_URL"], os.environ["SERVING_TOKEN"]) + + def create_workflow(self, workflow_id=None, workflow_key=None): + return Workflow( + self.request_handler, workflow_id=workflow_id, - wf_key=workflow_key, + workflow_key=workflow_key, ) diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index fa73ea1b0e..538956baad 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -1,23 +1,39 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from .sdk import EasyDataSDK - - -def workflow_scheduler(params, workflow_id: int) -> dict: - sdk = EasyDataSDK(workflow_id=workflow_id) - - return sdk.workflow.start(params) - - -def workflow_status_checker(workflow_key: int): - sdk = EasyDataSDK(wf_key=workflow_key) - workflow_status = sdk.workflow.get_status()["workflow_status"] - - return workflow_status - - -def workflow_data_retriever(workflow_key: int) -> str: - sdk = EasyDataSDK(wf_key=workflow_key) - - return sdk.workflow.result() +from tools.sdk import EasyDataSDK +import time + +def workflow_executor(params, workflow_id: int) -> dict: + sdk = EasyDataSDK() + workflow = sdk.create_workflow(workflow_id) + + start_workflow = workflow.start(params) + print(start_workflow) + + def check_workflow(): + workflow_status = workflow.get_status()["workflow_status"] + if workflow_status=="finished": + message = "Workflow finished." + elif workflow_status=="initializing" or workflow_status=="running": + message = "Workflow execution is still in progress." + else: + message = "Workflow has failed." + + return workflow_status, message + + MAX_RETRY = 50 + num_retry = 0 + while num_retry < MAX_RETRY: + workflow_status, message = check_workflow() + print(message) + if workflow_status == "failed" or workflow_status == "finished": + break + else: + time.sleep(100) # interval between each status checking retry + num_retry += 1 + + if workflow_status == "finished": + return workflow.result() + else: + return message diff --git a/WorkflowExecAgent/tools/tools.yaml b/WorkflowExecAgent/tools/tools.yaml index 60985817c0..8c79bff0ff 100644 --- a/WorkflowExecAgent/tools/tools.yaml +++ b/WorkflowExecAgent/tools/tools.yaml @@ -1,32 +1,14 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -workflow_scheduler: - description: "Used to start the workflow with a specified id" - callable_api: tools.py:workflow_scheduler +workflow_executor: + description: "Starts a workflow with the given workflow id and params. Gets the output result of the workflow." + callable_api: tools.py:workflow_executor args_schema: workflow_id: type: int description: Workflow id params: - type: Dict[str, str] - description: Workflow parameters. Dictionary keys can have whitespace - return_output: workflow_status - -workflow_status_checker: - description: "Used to check the execution status of the workflow." - callable_api: tools.py:workflow_status_checker - args_schema: - workflow_key: - type: int - description: Workflow key - return_output: workflow_status - -workflow_data_retriever: - description: "Used to retrieve workflow output data." - callable_api: tools.py:workflow_data_retriever - args_schema: - workflow_key: - type: int - description: Workflow key - return_output: workflow_output_data + type: dict[str, str] + description: Workflow parameters. + return_output: workflow_data diff --git a/WorkflowExecAgent/tools/utils/handle_requests.py b/WorkflowExecAgent/tools/utils/handle_requests.py index caa5c8bde8..e9806d09d3 100644 --- a/WorkflowExecAgent/tools/utils/handle_requests.py +++ b/WorkflowExecAgent/tools/utils/handle_requests.py @@ -56,7 +56,6 @@ def _handle_request(self, func): def decorated(response=None, *args, **kwargs): if response is not None: try: - print(response) return func(response, *args, **kwargs) except requests.exceptions.HTTPError as errh: From 00de5a85cd1621ec9a0efbaad9cbe0026ae0579b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:56:10 +0000 Subject: [PATCH 06/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- WorkflowExecAgent/README.md | 12 ++++-------- WorkflowExecAgent/docker_image_build/build.yaml | 2 +- WorkflowExecAgent/tests/1_build_images.sh | 2 +- WorkflowExecAgent/tests/2_start_vllm_service.sh | 4 ++-- .../tests/3_launch_and_validate_agent.sh | 2 +- WorkflowExecAgent/tests/test_compose.sh | 5 ++++- WorkflowExecAgent/tools/components/workflow.py | 1 + WorkflowExecAgent/tools/sdk.py | 1 + WorkflowExecAgent/tools/tools.py | 10 ++++++---- 9 files changed, 21 insertions(+), 18 deletions(-) diff --git a/WorkflowExecAgent/README.md b/WorkflowExecAgent/README.md index 67c4896e02..0a4b7f333e 100644 --- a/WorkflowExecAgent/README.md +++ b/WorkflowExecAgent/README.md @@ -6,7 +6,7 @@ GenAI Workflow Executor Example showcases the capability to handle data/AI workf ### Workflow Executor -This example demonstrates a single React-LangGraph with a `Workflow Executor` tool to ingest a user prompt to execute workflows and return an agent reasoning response based on the workflow output data. +This example demonstrates a single React-LangGraph with a `Workflow Executor` tool to ingest a user prompt to execute workflows and return an agent reasoning response based on the workflow output data. First the LLM extracts the relevant information from the user query based on the schema of the tool in `tools/tools.yaml`. Then the agent sends this `AgentState` to the `Workflow Executor` tool. @@ -30,9 +30,9 @@ As an example, here we have a Churn Prediction use-case workflow as the serving ![image](https://github.com/user-attachments/assets/c067f8b3-86cf-4abc-a8bd-51a98de8172d) -The workflow contains 2 paths which can be seen in the workflow illustrated, the top path and bottom path. +The workflow contains 2 paths which can be seen in the workflow illustrated, the top path and bottom path. -1. Top path - The training path which ends at the random forest classifier node is the training path. The data is cleaned through a series of nodes and used to train a random forest model for prediction. +1. Top path - The training path which ends at the random forest classifier node is the training path. The data is cleaned through a series of nodes and used to train a random forest model for prediction. 2. Bottom path - The inference path where trained random forest model is used for inferencing based on input parameter. @@ -59,11 +59,7 @@ curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: app The user has to provide a `workflow_id` and workflow `params` in the query. `workflow_id` a unique id used for serving the workflow to the microservice. Notice that the `query` string includes all the workflow `params` which the user has defined in the workflow. The LLM will extract these parameters into a dictionary format for the workflow `Serving Parameters` as shown below: ```python -params = { - "gender": "Female", - "tenure": 55, - "MonthlyAvgCharges": 103.7 -} +params = {"gender": "Female", "tenure": 55, "MonthlyAvgCharges": 103.7} ``` These parameters will be passed into the `Workflow Executor` tool to start the workflow execution of specified `workflow_id`. Thus, everything will be handled via the microservice. diff --git a/WorkflowExecAgent/docker_image_build/build.yaml b/WorkflowExecAgent/docker_image_build/build.yaml index e0c8a75b06..e2a778b9aa 100644 --- a/WorkflowExecAgent/docker_image_build/build.yaml +++ b/WorkflowExecAgent/docker_image_build/build.yaml @@ -10,4 +10,4 @@ services: http_proxy: ${http_proxy} https_proxy: ${https_proxy} no_proxy: ${no_proxy} - image: ${REGISTRY:-opea}/agent-langchain:${TAG:-latest} \ No newline at end of file + image: ${REGISTRY:-opea}/agent-langchain:${TAG:-latest} diff --git a/WorkflowExecAgent/tests/1_build_images.sh b/WorkflowExecAgent/tests/1_build_images.sh index 96af4139bd..ebb4883f44 100644 --- a/WorkflowExecAgent/tests/1_build_images.sh +++ b/WorkflowExecAgent/tests/1_build_images.sh @@ -26,4 +26,4 @@ function main() { echo "==================== Build agent docker image completed ====================" } -main \ No newline at end of file +main diff --git a/WorkflowExecAgent/tests/2_start_vllm_service.sh b/WorkflowExecAgent/tests/2_start_vllm_service.sh index 8aa0c687b0..b4e23a5d6b 100644 --- a/WorkflowExecAgent/tests/2_start_vllm_service.sh +++ b/WorkflowExecAgent/tests/2_start_vllm_service.sh @@ -32,7 +32,7 @@ function build_vllm_docker_image() { function start_vllm_service() { echo "start vllm service" - docker run -d -p ${vllm_port}:8083 --rm --network=host --name test-comps-vllm-service -v ~/.cache/huggingface:/root/.cache/huggingface -v ${WORKPATH}/tests/tool_chat_template_mistral_custom.jinja:/root/tool_chat_template_mistral_custom.jinja -e HF_TOKEN=$HF_TOKEN -e http_proxy=$http_proxy -e https_proxy=$https_proxy -it vllm-cpu-env --model ${model} --port 8083 --chat-template /root/tool_chat_template_mistral_custom.jinja --enable-auto-tool-choice --tool-call-parser mistral + docker run -d -p ${vllm_port}:8083 --rm --network=host --name test-comps-vllm-service -v ~/.cache/huggingface:/root/.cache/huggingface -v ${WORKPATH}/tests/tool_chat_template_mistral_custom.jinja:/root/tool_chat_template_mistral_custom.jinja -e HF_TOKEN=$HF_TOKEN -e http_proxy=$http_proxy -e https_proxy=$https_proxy -it vllm-cpu-env --model ${model} --port 8083 --chat-template /root/tool_chat_template_mistral_custom.jinja --enable-auto-tool-choice --tool-call-parser mistral echo ${LOG_PATH}/vllm-service.log sleep 5s echo "Waiting vllm ready" @@ -63,4 +63,4 @@ function main() { echo "==================== Start vllm docker service completed ====================" } -main \ No newline at end of file +main diff --git a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh index f826ad1ff0..4ff3c235d1 100644 --- a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh +++ b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh @@ -62,4 +62,4 @@ function main() { echo "==================== Agent service validated ====================" } -main \ No newline at end of file +main diff --git a/WorkflowExecAgent/tests/test_compose.sh b/WorkflowExecAgent/tests/test_compose.sh index 14d7c711ad..d1faa05a85 100644 --- a/WorkflowExecAgent/tests/test_compose.sh +++ b/WorkflowExecAgent/tests/test_compose.sh @@ -1,3 +1,6 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + function stop_agent_and_api_server() { echo "Stopping Agent services" docker rm --force $(docker ps -a -q --filter="name=workflowexec-agent-endpoint") @@ -27,4 +30,4 @@ stop_agent_and_api_server stop_vllm_docker echo "=================== #4 Agent and API server stopped ====================" -echo "ALL DONE!" \ No newline at end of file +echo "ALL DONE!" diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py index 48f4e7d432..7ac1863c26 100644 --- a/WorkflowExecAgent/tools/components/workflow.py +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -6,6 +6,7 @@ from tools.components.component import Component + class Workflow(Component): """Class for handling EasyData workflow operations. diff --git a/WorkflowExecAgent/tools/sdk.py b/WorkflowExecAgent/tools/sdk.py index 81349e5708..30838887f5 100644 --- a/WorkflowExecAgent/tools/sdk.py +++ b/WorkflowExecAgent/tools/sdk.py @@ -6,6 +6,7 @@ from tools.components.workflow import Workflow from tools.utils.handle_requests import RequestHandler + class EasyDataSDK: def __init__(self): self.request_handler = RequestHandler(os.environ["SDK_BASE_URL"], os.environ["SERVING_TOKEN"]) diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index 538956baad..070a14678c 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -1,21 +1,23 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from tools.sdk import EasyDataSDK import time +from tools.sdk import EasyDataSDK + + def workflow_executor(params, workflow_id: int) -> dict: sdk = EasyDataSDK() workflow = sdk.create_workflow(workflow_id) start_workflow = workflow.start(params) print(start_workflow) - + def check_workflow(): workflow_status = workflow.get_status()["workflow_status"] - if workflow_status=="finished": + if workflow_status == "finished": message = "Workflow finished." - elif workflow_status=="initializing" or workflow_status=="running": + elif workflow_status == "initializing" or workflow_status == "running": message = "Workflow execution is still in progress." else: message = "Workflow has failed." From 7f8f9575c3b7811e468a1da41aedcd66ed355ae6 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Tue, 22 Oct 2024 02:52:04 +0000 Subject: [PATCH 07/21] Rename CI script to 'test_compose_on_xeon.sh' Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/tests/README.md | 4 ++-- .../tests/{test_compose.sh => test_compose_on_xeon.sh} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename WorkflowExecAgent/tests/{test_compose.sh => test_compose_on_xeon.sh} (100%) diff --git a/WorkflowExecAgent/tests/README.md b/WorkflowExecAgent/tests/README.md index 0886880006..1dbaab6e93 100644 --- a/WorkflowExecAgent/tests/README.md +++ b/WorkflowExecAgent/tests/README.md @@ -24,10 +24,10 @@ Launch validation by running the following command. ```sh cd GenAIExamples/WorkflowExecAgent/tests -. /test_compose.sh +. /test_compose_on_xeon.sh ``` -`test_compose.sh` will run the other `.sh` files under `tests/`. The validation script launches 1 docker container for the agent microservice, and another for the vllm model serving on CPU. When validation is completed, all containers will be stopped. +`test_compose_on_xeon.sh` will run the other `.sh` files under `tests/`. The validation script launches 1 docker container for the agent microservice, and another for the vllm model serving on CPU. When validation is completed, all containers will be stopped. The validation is tested by checking if the model reasoning output response matches a partial substring. The expected output is shown below: diff --git a/WorkflowExecAgent/tests/test_compose.sh b/WorkflowExecAgent/tests/test_compose_on_xeon.sh similarity index 100% rename from WorkflowExecAgent/tests/test_compose.sh rename to WorkflowExecAgent/tests/test_compose_on_xeon.sh From 14944f91b4767baf0f71d5efec7a07607b628c3b Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Wed, 23 Oct 2024 11:19:03 +0000 Subject: [PATCH 08/21] Update test files and add custom prompt Signed-off-by: JoshuaL3000 --- .../cpu/xeon/compose_vllm.yaml} | 1 + WorkflowExecAgent/tests/2_start_vllm_service.sh | 6 +++--- .../tests/3_launch_and_validate_agent.sh | 11 ++++++----- ...ompose_on_xeon.sh => test_compose_vllm_on_xeon.sh} | 0 WorkflowExecAgent/tools/custom_prompt.py | 6 ++++++ 5 files changed, 16 insertions(+), 8 deletions(-) rename WorkflowExecAgent/docker_compose/{docker_compose.yaml => intel/cpu/xeon/compose_vllm.yaml} (94%) rename WorkflowExecAgent/tests/{test_compose_on_xeon.sh => test_compose_vllm_on_xeon.sh} (100%) create mode 100644 WorkflowExecAgent/tools/custom_prompt.py diff --git a/WorkflowExecAgent/docker_compose/docker_compose.yaml b/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml similarity index 94% rename from WorkflowExecAgent/docker_compose/docker_compose.yaml rename to WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml index 913dd03210..6fede271a8 100644 --- a/WorkflowExecAgent/docker_compose/docker_compose.yaml +++ b/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml @@ -28,3 +28,4 @@ services: port: 9090 SDK_BASE_URL: ${SDK_BASE_URL} SERVING_TOKEN: ${SERVING_TOKEN} + custom_prompt: /home/user/tools/custom_prompt.py diff --git a/WorkflowExecAgent/tests/2_start_vllm_service.sh b/WorkflowExecAgent/tests/2_start_vllm_service.sh index b4e23a5d6b..ec8915df89 100644 --- a/WorkflowExecAgent/tests/2_start_vllm_service.sh +++ b/WorkflowExecAgent/tests/2_start_vllm_service.sh @@ -7,10 +7,10 @@ set -e WORKPATH=$(dirname "$PWD") LOG_PATH="$WORKPATH/tests" vllm_port=${vllm_port} +[[ -z "$vllm_port" ]] && vllm_port=8084 model=mistralai/Mistral-7B-Instruct-v0.3 export WORKDIR=$WORKPATH/../../ -export HF_TOKEN=${HF_TOKEN} -export VLLM_CPU_OMP_THREADS_BIND=${VLLM_CPU_OMP_THREADS_BIND} +export HF_TOKEN=${HUGGINGFACEHUB_API_TOKEN} function build_vllm_docker_image() { echo "Building the vllm docker images" @@ -32,7 +32,7 @@ function build_vllm_docker_image() { function start_vllm_service() { echo "start vllm service" - docker run -d -p ${vllm_port}:8083 --rm --network=host --name test-comps-vllm-service -v ~/.cache/huggingface:/root/.cache/huggingface -v ${WORKPATH}/tests/tool_chat_template_mistral_custom.jinja:/root/tool_chat_template_mistral_custom.jinja -e HF_TOKEN=$HF_TOKEN -e http_proxy=$http_proxy -e https_proxy=$https_proxy -it vllm-cpu-env --model ${model} --port 8083 --chat-template /root/tool_chat_template_mistral_custom.jinja --enable-auto-tool-choice --tool-call-parser mistral + docker run -d -p ${vllm_port}:${vllm_port} --rm --network=host --name test-comps-vllm-service -v ~/.cache/huggingface:/root/.cache/huggingface -v ${WORKPATH}/tests/tool_chat_template_mistral_custom.jinja:/root/tool_chat_template_mistral_custom.jinja -e HF_TOKEN=$HF_TOKEN -e http_proxy=$http_proxy -e https_proxy=$https_proxy -it vllm-cpu-env --model ${model} --port ${vllm_port} --chat-template /root/tool_chat_template_mistral_custom.jinja --enable-auto-tool-choice --tool-call-parser mistral echo ${LOG_PATH}/vllm-service.log sleep 5s echo "Waiting vllm ready" diff --git a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh index 4ff3c235d1..2fb7b40300 100644 --- a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh +++ b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh @@ -5,16 +5,17 @@ set -e WORKPATH=$(dirname "$PWD") -workflow_id=${workflow_id} +workflow_id=9794 vllm_port=${vllm_port} +[[ -z "$vllm_port" ]] && vllm_port=8084 export WORKDIR=$WORKPATH/../../ echo "WORKDIR=${WORKDIR}" export SDK_BASE_URL=${SDK_BASE_URL} export SERVING_TOKEN=${SERVING_TOKEN} -export HF_TOKEN=${HF_TOKEN} +export HF_TOKEN=${HUGGINGFACEHUB_API_TOKEN} export llm_engine=vllm export ip_address=$(hostname -I | awk '{print $1}') -export llm_endpoint_url=${ip_address}:${vllm_port} +export llm_endpoint_url=http://${ip_address}:${vllm_port} export model=mistralai/Mistral-7B-Instruct-v0.3 export recursion_limit=25 export temperature=0 @@ -23,8 +24,8 @@ export TOOLSET_PATH=$WORKDIR/GenAIExamples/WorkflowExecAgent/tools/ function start_agent_and_api_server() { echo "Starting Agent services" - cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_compose - WORKDIR=$WORKPATH/docker_image_build/ docker compose -f docker_compose.yaml up -d + cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_compose/intel/cpu/xeon + WORKDIR=$WORKPATH/docker_image_build/ docker compose -f compose_vllm.yaml up -d echo "Waiting agent service ready" sleep 5s } diff --git a/WorkflowExecAgent/tests/test_compose_on_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh similarity index 100% rename from WorkflowExecAgent/tests/test_compose_on_xeon.sh rename to WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh diff --git a/WorkflowExecAgent/tools/custom_prompt.py b/WorkflowExecAgent/tools/custom_prompt.py new file mode 100644 index 0000000000..8840c12511 --- /dev/null +++ b/WorkflowExecAgent/tools/custom_prompt.py @@ -0,0 +1,6 @@ +REACT_SYS_MESSAGE = """\ +You are a helpful assistant. You are to start the workflow using the tool provided. +After the workflow is completed, you will use the output data to answer the user's original question in a one short sentence. + +Now begin! +""" From 891324ff4097dbf60275b2539244dc573a304fb6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:39:05 +0000 Subject: [PATCH 09/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- WorkflowExecAgent/tools/custom_prompt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WorkflowExecAgent/tools/custom_prompt.py b/WorkflowExecAgent/tools/custom_prompt.py index 8840c12511..bdad5d6b92 100644 --- a/WorkflowExecAgent/tools/custom_prompt.py +++ b/WorkflowExecAgent/tools/custom_prompt.py @@ -1,3 +1,6 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + REACT_SYS_MESSAGE = """\ You are a helpful assistant. You are to start the workflow using the tool provided. After the workflow is completed, you will use the output data to answer the user's original question in a one short sentence. From f8f3afd53b7b4c3f9b36707de30fe01a89d94e67 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Fri, 25 Oct 2024 01:58:45 +0000 Subject: [PATCH 10/21] Rename test script Signed-off-by: JoshuaL3000 --- .../{test_compose_vllm_on_xeon.sh => test_compose_on_xeon.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename WorkflowExecAgent/tests/{test_compose_vllm_on_xeon.sh => test_compose_on_xeon.sh} (100%) diff --git a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh b/WorkflowExecAgent/tests/test_compose_on_xeon.sh similarity index 100% rename from WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh rename to WorkflowExecAgent/tests/test_compose_on_xeon.sh From 62fd8634651554436a006cfd9873bd7192b89ac4 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Fri, 25 Oct 2024 07:57:32 +0000 Subject: [PATCH 11/21] Fix start_vllm_service.sh and handle convert dict to str in tools.py Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/tests/2_start_vllm_service.sh | 3 ++- .../{test_compose_on_xeon.sh => test_compose_vllm_on_xeon.sh} | 0 WorkflowExecAgent/tools/tools.py | 1 + WorkflowExecAgent/tools/tools.yaml | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) rename WorkflowExecAgent/tests/{test_compose_on_xeon.sh => test_compose_vllm_on_xeon.sh} (100%) diff --git a/WorkflowExecAgent/tests/2_start_vllm_service.sh b/WorkflowExecAgent/tests/2_start_vllm_service.sh index ec8915df89..2c34253284 100644 --- a/WorkflowExecAgent/tests/2_start_vllm_service.sh +++ b/WorkflowExecAgent/tests/2_start_vllm_service.sh @@ -19,8 +19,9 @@ function build_vllm_docker_image() { if [ ! -d "./vllm" ]; then git clone https://github.com/vllm-project/vllm.git cd ./vllm; git checkout tags/v0.6.0 + else + cd ./vllm fi - cd ./vllm docker build -f Dockerfile.cpu -t vllm-cpu-env --shm-size=100g . if [ $? -ne 0 ]; then echo "opea/vllm:cpu failed" diff --git a/WorkflowExecAgent/tests/test_compose_on_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh similarity index 100% rename from WorkflowExecAgent/tests/test_compose_on_xeon.sh rename to WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index 070a14678c..4a9c98909e 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -10,6 +10,7 @@ def workflow_executor(params, workflow_id: int) -> dict: sdk = EasyDataSDK() workflow = sdk.create_workflow(workflow_id) + params = {key: str(val) for key, val in params.items()} start_workflow = workflow.start(params) print(start_workflow) diff --git a/WorkflowExecAgent/tools/tools.yaml b/WorkflowExecAgent/tools/tools.yaml index 8c79bff0ff..c326d55063 100644 --- a/WorkflowExecAgent/tools/tools.yaml +++ b/WorkflowExecAgent/tools/tools.yaml @@ -9,6 +9,6 @@ workflow_executor: type: int description: Workflow id params: - type: dict[str, str] + type: dict description: Workflow parameters. return_output: workflow_data From d251aa569aa6cb925d9c07a20b95f6f524dfddbe Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Mon, 28 Oct 2024 01:44:25 +0000 Subject: [PATCH 12/21] Update workflow id and retest pydantic version Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/tests/3_launch_and_validate_agent.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh index 2fb7b40300..5c9e6da583 100644 --- a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh +++ b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh @@ -5,7 +5,7 @@ set -e WORKPATH=$(dirname "$PWD") -workflow_id=9794 +workflow_id=9809 vllm_port=${vllm_port} [[ -z "$vllm_port" ]] && vllm_port=8084 export WORKDIR=$WORKPATH/../../ From bfb0522c947dfd002b1d419af4150e6e5910040d Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Tue, 5 Nov 2024 08:50:28 +0000 Subject: [PATCH 13/21] Add docstring for multiple files Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/tools/components/component.py | 11 +++++++++++ WorkflowExecAgent/tools/components/workflow.py | 3 ++- WorkflowExecAgent/tools/sdk.py | 17 +++++++++++++++-- WorkflowExecAgent/tools/tools.py | 17 ++++++++++++++--- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/WorkflowExecAgent/tools/components/component.py b/WorkflowExecAgent/tools/components/component.py index e0491aaa19..b2897c18b1 100644 --- a/WorkflowExecAgent/tools/components/component.py +++ b/WorkflowExecAgent/tools/components/component.py @@ -3,8 +3,19 @@ class Component: + """BaseClass for component objects to make API requests. + + Attributes: + request_handler: RequestHandler object + """ + def __init__(self, request_handler): self.request_handler = request_handler def _make_request(self, *args, **kwargs): + """Uses the request_handler object to make API requests. + + :returns: API response + """ + return self.request_handler._make_request(*args, **kwargs) diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py index 7ac1863c26..34c40fd521 100644 --- a/WorkflowExecAgent/tools/components/workflow.py +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -8,7 +8,7 @@ class Workflow(Component): - """Class for handling EasyData workflow operations. + """Class for handling workflow operations. Attributes: workflow_id: workflow id @@ -33,6 +33,7 @@ def start(self, params: Dict[str, str]) -> Dict[str, str]: :rtype: string """ + data = json.dumps({"params": params}) endpoint = f"serving/servable_workflows/{self.workflow_id}/start" self.wf_key = self._make_request(endpoint, "POST", data)["wf_key"] diff --git a/WorkflowExecAgent/tools/sdk.py b/WorkflowExecAgent/tools/sdk.py index 30838887f5..e04d1e2f15 100644 --- a/WorkflowExecAgent/tools/sdk.py +++ b/WorkflowExecAgent/tools/sdk.py @@ -7,11 +7,24 @@ from tools.utils.handle_requests import RequestHandler -class EasyDataSDK: +class EasyDataSDK: # Example SDK class for Data Insight Automation platform + """SDK class containing all components. + + Attributes: + request_handler: RequestHandler object + """ + def __init__(self): self.request_handler = RequestHandler(os.environ["SDK_BASE_URL"], os.environ["SERVING_TOKEN"]) - def create_workflow(self, workflow_id=None, workflow_key=None): + def create_workflow(self, workflow_id:int=None, workflow_key=None): + """Creates a Workflow object. + + :param int workflow_id: Servable workflow id. + + :returns: Workflow + """ + return Workflow( self.request_handler, workflow_id=workflow_id, diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index 4a9c98909e..2e1e18ee03 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -7,8 +7,19 @@ def workflow_executor(params, workflow_id: int) -> dict: - sdk = EasyDataSDK() - workflow = sdk.create_workflow(workflow_id) + """Workflow executor tool. Runs a workflow and returns the result. + + :param int workflow_id: Servable workflow id. + + :returns: workflow output results + + :rtype: dict + """ + + # Replace function logic with use-case + + sdk = EasyDataSDK() # Initialize SDK instance + workflow = sdk.create_workflow(workflow_id) # Create workflow instance object params = {key: str(val) for key, val in params.items()} start_workflow = workflow.start(params) @@ -33,7 +44,7 @@ def check_workflow(): if workflow_status == "failed" or workflow_status == "finished": break else: - time.sleep(100) # interval between each status checking retry + time.sleep(100) # interval between each status check retry num_retry += 1 if workflow_status == "finished": From b784a681e75c3d3a05efce9a89b948df50eba177 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Fri, 8 Nov 2024 04:25:58 +0000 Subject: [PATCH 14/21] Update workflow executor example for example workflow API * Add files for example workflow * Add test scripts for example workflow CICD * Update tool docstrings Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/tests/1_build_images.sh | 2 +- .../tests/3_launch_agent_service.sh | 39 +++++ .../tests/3_launch_example_wf_api.sh | 40 +++++ ..._validate_agent.sh => 4_validate_agent.sh} | 31 +--- .../Dockerfile.example_workflow_api | 11 ++ .../launch_workflow_service.sh | 15 ++ .../tests/example_workflow/main.py | 55 +++++++ .../tests/example_workflow/requirements.txt | 7 + .../tests/example_workflow/workflow.py | 137 ++++++++++++++++++ .../test_compose_vllm_example_wf_xeon.sh | 49 +++++++ .../tests/test_compose_vllm_on_xeon.sh | 22 ++- .../tools/components/workflow.py | 2 +- WorkflowExecAgent/tools/sdk.py | 2 +- WorkflowExecAgent/tools/tools.py | 4 +- 14 files changed, 377 insertions(+), 39 deletions(-) create mode 100644 WorkflowExecAgent/tests/3_launch_agent_service.sh create mode 100644 WorkflowExecAgent/tests/3_launch_example_wf_api.sh rename WorkflowExecAgent/tests/{3_launch_and_validate_agent.sh => 4_validate_agent.sh} (50%) create mode 100644 WorkflowExecAgent/tests/example_workflow/Dockerfile.example_workflow_api create mode 100644 WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh create mode 100644 WorkflowExecAgent/tests/example_workflow/main.py create mode 100644 WorkflowExecAgent/tests/example_workflow/requirements.txt create mode 100644 WorkflowExecAgent/tests/example_workflow/workflow.py create mode 100644 WorkflowExecAgent/tests/test_compose_vllm_example_wf_xeon.sh diff --git a/WorkflowExecAgent/tests/1_build_images.sh b/WorkflowExecAgent/tests/1_build_images.sh index ebb4883f44..85ea4ab097 100644 --- a/WorkflowExecAgent/tests/1_build_images.sh +++ b/WorkflowExecAgent/tests/1_build_images.sh @@ -17,7 +17,7 @@ function build_agent_docker_image() { cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_image_build/ get_genai_comps echo "Build agent image with --no-cache..." - docker compose -f build.yaml build --no-cache + docker compose -f build.yaml build } function main() { diff --git a/WorkflowExecAgent/tests/3_launch_agent_service.sh b/WorkflowExecAgent/tests/3_launch_agent_service.sh new file mode 100644 index 0000000000..0d7f53da6f --- /dev/null +++ b/WorkflowExecAgent/tests/3_launch_agent_service.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +WORKPATH=$(dirname "$PWD") +workflow_id=9809 +vllm_port=${vllm_port} +[[ -z "$vllm_port" ]] && vllm_port=8084 +export WORKDIR=$WORKPATH/../../ +echo "WORKDIR=${WORKDIR}" +export SDK_BASE_URL=$1 +export SERVING_TOKEN=${SERVING_TOKEN} +export HF_TOKEN=${HUGGINGFACEHUB_API_TOKEN} +export llm_engine=vllm +export ip_address=$(hostname -I | awk '{print $1}') +export llm_endpoint_url=http://${ip_address}:${vllm_port} +export model=mistralai/Mistral-7B-Instruct-v0.3 +export recursion_limit=25 +export temperature=0 +export max_new_tokens=1000 +export TOOLSET_PATH=$WORKDIR/GenAIExamples/WorkflowExecAgent/tools/ + +function start_agent() { + echo "Starting Agent services" + cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_compose/intel/cpu/xeon + WORKDIR=$WORKPATH/docker_image_build/ docker compose -f compose_vllm.yaml up -d + echo "Waiting agent service ready" + sleep 5s +} + +function main() { + echo "==================== Start agent service ====================" + start_agent + echo "==================== Agent service started ====================" +} + +main diff --git a/WorkflowExecAgent/tests/3_launch_example_wf_api.sh b/WorkflowExecAgent/tests/3_launch_example_wf_api.sh new file mode 100644 index 0000000000..8d736c2935 --- /dev/null +++ b/WorkflowExecAgent/tests/3_launch_example_wf_api.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +wf_api_port=${wf_api_port} +[[ -z "$wf_api_port" ]] && wf_api_port=5000 +WORKPATH=$(dirname "$PWD") +LOG_PATH="$WORKPATH/tests/example_workflow" +export WORKDIR=$WORKPATH/../../ +echo "WORKDIR=${WORKDIR}" + +function start_example_workflow_api() { + echo "Starting example workflow API" + cd $WORKDIR/GenAIExamples/WorkflowExecAgent/tests/example_workflow + docker build -f Dockerfile.example_workflow_api -t example-workflow-service . + docker run -d -p ${wf_api_port}:${wf_api_port} --rm --network=host --name example-workflow-service -it example-workflow-service + echo "Waiting example workflow API ready" + until [[ "$n" -ge 100 ]] || [[ $ready == true ]]; do + docker logs example-workflow-service &> ${LOG_PATH}/example-workflow-service.log + n=$((n+1)) + if grep -q "Uvicorn running on" ${LOG_PATH}/example-workflow-service.log; then + break + fi + if grep -q "No such container" ${LOG_PATH}/example-workflow-service.log; then + echo "container example-workflow-service not found" + exit 1 + fi + sleep 5s + done +} + +function main() { + echo "==================== Start example workflow API ====================" + start_example_workflow_api + echo "==================== Example workflow API started ====================" +} + +main diff --git a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh b/WorkflowExecAgent/tests/4_validate_agent.sh similarity index 50% rename from WorkflowExecAgent/tests/3_launch_and_validate_agent.sh rename to WorkflowExecAgent/tests/4_validate_agent.sh index 5c9e6da583..ad9e9342c9 100644 --- a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh +++ b/WorkflowExecAgent/tests/4_validate_agent.sh @@ -5,30 +5,11 @@ set -e WORKPATH=$(dirname "$PWD") -workflow_id=9809 -vllm_port=${vllm_port} -[[ -z "$vllm_port" ]] && vllm_port=8084 export WORKDIR=$WORKPATH/../../ echo "WORKDIR=${WORKDIR}" -export SDK_BASE_URL=${SDK_BASE_URL} -export SERVING_TOKEN=${SERVING_TOKEN} -export HF_TOKEN=${HUGGINGFACEHUB_API_TOKEN} -export llm_engine=vllm export ip_address=$(hostname -I | awk '{print $1}') -export llm_endpoint_url=http://${ip_address}:${vllm_port} -export model=mistralai/Mistral-7B-Instruct-v0.3 -export recursion_limit=25 -export temperature=0 -export max_new_tokens=1000 -export TOOLSET_PATH=$WORKDIR/GenAIExamples/WorkflowExecAgent/tools/ - -function start_agent_and_api_server() { - echo "Starting Agent services" - cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_compose/intel/cpu/xeon - WORKDIR=$WORKPATH/docker_image_build/ docker compose -f compose_vllm.yaml up -d - echo "Waiting agent service ready" - sleep 5s -} +query=$1 +validate_result=$2 function validate() { local CONTENT="$1" @@ -47,17 +28,13 @@ function validate() { function validate_agent_service() { echo "----------------Test agent ----------------" local CONTENT=$(curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ - "query": "I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." + "query": "'"${query}"'" }') - validate "$CONTENT" "The entry is not likely to churn" "workflowexec-agent-endpoint" + validate "$CONTENT" "$validate_result" "workflowexec-agent-endpoint" docker logs workflowexec-agent-endpoint } function main() { - echo "==================== Start agent ====================" - start_agent_and_api_server - echo "==================== Agent started ====================" - echo "==================== Validate agent service ====================" validate_agent_service echo "==================== Agent service validated ====================" diff --git a/WorkflowExecAgent/tests/example_workflow/Dockerfile.example_workflow_api b/WorkflowExecAgent/tests/example_workflow/Dockerfile.example_workflow_api new file mode 100644 index 0000000000..0395b2f3f9 --- /dev/null +++ b/WorkflowExecAgent/tests/example_workflow/Dockerfile.example_workflow_api @@ -0,0 +1,11 @@ +FROM ubuntu:22.04 + +RUN apt-get -qq update + +WORKDIR /home/ubuntu + +COPY launch_workflow_service.sh requirements.txt main.py workflow.py ./ + +RUN chmod +x ./launch_workflow_service.sh + +CMD ["./launch_workflow_service.sh"] diff --git a/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh b/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh new file mode 100644 index 0000000000..9367aaf28d --- /dev/null +++ b/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +apt-get -qq -y install --no-install-recommends unzip curl ca-certificates +apt-get -qq -y install --no-install-recommends python3 python3-pip + +curl -L -o ./archive.zip https://www.kaggle.com/api/v1/datasets/download/blastchar/telco-customer-churn +unzip ./archive.zip -d ./ +rm ./archive.zip + +pip install virtualenv && \ + virtualenv venv && \ + source venv/bin/activate && \ + pip install -r requirements.txt + +uvicorn main:app --reload --port=5000 --host=0.0.0.0 diff --git a/WorkflowExecAgent/tests/example_workflow/main.py b/WorkflowExecAgent/tests/example_workflow/main.py new file mode 100644 index 0000000000..81ac51e252 --- /dev/null +++ b/WorkflowExecAgent/tests/example_workflow/main.py @@ -0,0 +1,55 @@ +import logging +from fastapi import FastAPI, APIRouter + +from workflow import run_workflow + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +app = FastAPI() + +router = APIRouter( + prefix="/serving", + tags=["Workflow Serving"] +) + +app.results = {} + +@router.post('/servable_workflows/{wf_id}/start', summary="Start Workflow") +async def start_workflow(wf_id: int, params: dict): + try: + app.results = run_workflow(params["params"]) + wf_key = "example_key" + return {"msg": "ok", "wf_key": wf_key} + + except Exception as e: + logging.error(e, exc_info=True) + return {"msg": "error occured"} + +@router.get('/serving_workflows/{wf_key}/status', summary="Get Workflow Status") +async def get_status(wf_key: str): + try: + if app.results: + status = "finished" + else: + status = "failed" + + return {"workflow_status": status} + except Exception as e: + logging.error(e) + return {"msg": "error occured"} + +@router.get('/serving_workflows/{wf_key}/results', summary = "Get Workflow Results") +async def get_results(wf_key: str): + try: + if app.results: + return app.results + else: + return {"msg": "There is an issue while getting results !!"} + + except Exception as e: + logging.error(e) + return {"msg": "There is an issue while getting results !!"} + +app.include_router(router) diff --git a/WorkflowExecAgent/tests/example_workflow/requirements.txt b/WorkflowExecAgent/tests/example_workflow/requirements.txt new file mode 100644 index 0000000000..fb6a484874 --- /dev/null +++ b/WorkflowExecAgent/tests/example_workflow/requirements.txt @@ -0,0 +1,7 @@ +fastapi +numpy +pandas +requests==2.28.1 +scikit-learn +uvicorn +xgboost diff --git a/WorkflowExecAgent/tests/example_workflow/workflow.py b/WorkflowExecAgent/tests/example_workflow/workflow.py new file mode 100644 index 0000000000..5697a33248 --- /dev/null +++ b/WorkflowExecAgent/tests/example_workflow/workflow.py @@ -0,0 +1,137 @@ +import json +import warnings +warnings.filterwarnings('ignore') + +import pandas as pd +from sklearn.preprocessing import MinMaxScaler, LabelEncoder +from sklearn.tree import DecisionTreeClassifier +from sklearn.naive_bayes import GaussianNB +from sklearn.neighbors import KNeighborsClassifier +from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier, GradientBoostingClassifier +from sklearn.svm import SVC +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split, GridSearchCV +from sklearn.metrics import accuracy_score +from sklearn.pipeline import Pipeline +from xgboost import XGBClassifier + + +def churn_prediction(params): + df = pd.read_csv('./WA_Fn-UseC_-Telco-Customer-Churn.csv') + + # Data Cleaning + df = df.drop(['customerID'], axis=1) + select_cols = ["gender", "tenure", "MonthlyCharges", "TotalCharges", "Churn"] + df = df[select_cols] + + df['TotalCharges'] = pd.to_numeric(df.TotalCharges, errors='coerce') + df.drop(labels=df[df['tenure'] == 0].index, axis=0, inplace=True) + df.fillna(df["TotalCharges"].mean()) + + # Data Preprocessing + encoders = {} + def object_to_int(dataframe_series): + if dataframe_series.dtype=='object': + encoders[dataframe_series.name] = LabelEncoder().fit(dataframe_series) + dataframe_series = encoders[dataframe_series.name].transform(dataframe_series) + return dataframe_series + + df = df.apply(lambda x: object_to_int(x)) + X = df.drop(columns=['Churn']) + y = df['Churn'].values + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=40, stratify=y) + + model_scores = [] + + models = [ + ('Random Forest', RandomForestClassifier(random_state=42), + {'model__n_estimators': [50, 100, 200], + 'model__max_depth': [None, 10, 20]}), # Add hyperparameters for Random Forest + ('Gradient Boosting', GradientBoostingClassifier(random_state=42), + {'model__n_estimators': [50, 100, 200], + 'model__learning_rate': [0.05, 0.1, 0.5]}), # Add hyperparameters for Gradient Boosting + ('Support Vector Machine', SVC(random_state=42, class_weight='balanced'), + {'model__C': [0.1, 1, 10], + 'model__gamma': ['scale', 'auto']}), # Add hyperparameters for SVM + ('Logistic Regression', LogisticRegression(random_state=42, class_weight='balanced'), + {'model__C': [0.1, 1, 10], + 'model__penalty': ['l1', 'l2']}), # Add hyperparameters for Logistic Regression + ('K-Nearest Neighbors', KNeighborsClassifier(), + {'model__n_neighbors': [3, 5, 7], + 'model__weights': ['uniform', 'distance']}), # Add hyperparameters for KNN + ('Decision Tree', DecisionTreeClassifier(random_state=42), + {'model__max_depth': [None, 10, 20], + 'model__min_samples_split': [2, 5, 10]}), # Add hyperparameters for Decision Tree + ('Ada Boost', AdaBoostClassifier(random_state=42), + {'model__n_estimators': [50, 100, 200], + 'model__learning_rate': [0.05, 0.1, 0.5]}), # Add hyperparameters for Ada Boost + ('XG Boost', XGBClassifier(random_state=42), + {'model__n_estimators': [50, 100, 200], + 'model__learning_rate': [0.05, 0.1, 0.5]}), # Add hyperparameters for XG Boost + ('Naive Bayes', GaussianNB(), {}) # No hyperparameters for Naive Bayes + ] + + best_model = None + best_accuracy = 0.0 + + for name, model, param_grid in models: + # Create a pipeline for each model + pipeline = Pipeline([ + ('scaler', MinMaxScaler()), # Feature Scaling + ('model', model) + ]) + + # Hyperparameter tuning using GridSearchCV + if param_grid: + grid_search = GridSearchCV(pipeline, param_grid, cv=2) + grid_search.fit(X_train, y_train) + pipeline = grid_search.best_estimator_ + + # Fit the pipeline on the training data + pipeline.fit(X_train, y_train) + + # Make predictions on the test data + y_pred = pipeline.predict(X_test) + + # Calculate accuracy score + accuracy = accuracy_score(y_test, y_pred) + + # Append model name and accuracy to the list + model_scores.append({'Model': name, 'Accuracy': accuracy}) + + # Convert the list to a DataFrame + scores_df = pd.DataFrame(model_scores) + print("Model:", name) + print("Test Accuracy:", round(accuracy, 3),"%\n") + + # Check if the current model has the best accuracy + if accuracy > best_accuracy: + best_accuracy = accuracy + best_model = pipeline + + # Retrieve the overall best model + print("Best Model:") + print("Test Accuracy:", best_accuracy) + print("Model Pipeline:", best_model, "with accuracy", round(best_accuracy, 2), "%\n") + + # Process and Predict input values from user + def transform_params(name, value): + return encoders[name].transform(value)[0] + + predict_input = {} + print("Predicting with user provided params:", params) + for key, value in params.items(): + if key in encoders.keys(): + predict_input[key] = transform_params(key, [value]) + else: + predict_input[key] = value + + predict_input = pd.DataFrame([predict_input]) + result = best_model.predict(predict_input) + params["prediction"] = encoders["Churn"].inverse_transform(result)[0] + result = json.dumps(params) + + return result + +def run_workflow(params): + return churn_prediction(params) diff --git a/WorkflowExecAgent/tests/test_compose_vllm_example_wf_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_example_wf_xeon.sh new file mode 100644 index 0000000000..82528548c0 --- /dev/null +++ b/WorkflowExecAgent/tests/test_compose_vllm_example_wf_xeon.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +wf_api_port=${wf_api_port} +[[ -z "$wf_api_port" ]] && wf_api_port=5000 +api_server_url=http://$(hostname -I | awk '{print $1}'):${wf_api_port}/ +query="I have a data with gender Female, tenure 55, MonthlyCharges 103.7, TotalCharges 1840.75. Predict if this entry will churn. My workflow id is ${workflow_id}." +validate_result="the prediction is No" + +function stop_agent_and_api_server() { + echo "Stopping Agent services" + docker rm --force $(docker ps -a -q --filter="name=workflowexec-agent-endpoint") + docker rm --force $(docker ps -a -q --filter="name=example-workflow-service") +} + +function stop_vllm_docker() { + cid=$(docker ps -aq --filter "name=test-comps-vllm-service") + echo "Stopping the docker containers "${cid} + if [[ ! -z "$cid" ]]; then docker rm $cid -f && sleep 1s; fi + echo "Docker containers stopped successfully" +} + +echo "=================== #1 Building docker images ====================" +bash 1_build_images.sh +echo "=================== #1 Building docker images completed ====================" + +echo "=================== #2 Start vllm service ====================" +bash 2_start_vllm_service.sh +echo "=================== #2 Start vllm service completed ====================" + +echo "=================== #3 Start agent service ====================" +bash 3_launch_agent_service.sh $api_server_url +echo "=================== #3 Agent service started ====================" + +echo "=================== #4 Start example workflow API ====================" +bash 3_launch_example_wf_api.sh +echo "=================== #4 Example workflow API started ====================" + +echo "=================== #5 Start validate agent ====================" +bash 4_validate_agent.sh "$query" "$validate_result" +echo "=================== #5 Validate agent completed ====================" + +echo "=================== #4 Stop all services ====================" +stop_agent_and_api_server +stop_vllm_docker +echo "=================== #4 All services stopped ====================" + +echo "ALL DONE!" diff --git a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh index d1faa05a85..a316fe0734 100644 --- a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh +++ b/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh @@ -1,7 +1,11 @@ +#!/bin/bash # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -function stop_agent_and_api_server() { +query="I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." +validate_result="The entry is not likely to churn" + +function stop_agent_server() { echo "Stopping Agent services" docker rm --force $(docker ps -a -q --filter="name=workflowexec-agent-endpoint") } @@ -21,13 +25,17 @@ echo "=================== #2 Start vllm service ====================" bash 2_start_vllm_service.sh echo "=================== #2 Start vllm service completed ====================" -echo "=================== #3 Start agent and API server ====================" -bash 3_launch_and_validate_agent.sh -echo "=================== #3 Agent test completed ====================" +echo "=================== #3 Start agent service ====================" +bash 3_launch_agent_service.sh $SDK_BASE_URL +echo "=================== #3 Agent service started ====================" + +echo "=================== #4 Start validate agent ====================" +bash 4_validate_agent.sh "$query" "$validate_result" +echo "=================== #4 Validate agent completed ====================" -echo "=================== #4 Stop agent and API server ====================" -stop_agent_and_api_server +echo "=================== #4 Stop agent and vllm server ====================" +stop_agent_server stop_vllm_docker -echo "=================== #4 Agent and API server stopped ====================" +echo "=================== #4 Agent and vllm server stopped ====================" echo "ALL DONE!" diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py index 34c40fd521..e68218b7fd 100644 --- a/WorkflowExecAgent/tools/components/workflow.py +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -36,7 +36,7 @@ def start(self, params: Dict[str, str]) -> Dict[str, str]: data = json.dumps({"params": params}) endpoint = f"serving/servable_workflows/{self.workflow_id}/start" - self.wf_key = self._make_request(endpoint, "POST", data)["wf_key"] + self.wf_key = self._make_request(endpoint, "POST", data).get("wf_key", None) if self.wf_key: return f"Workflow successfully started. The workflow key is {self.wf_key}." else: diff --git a/WorkflowExecAgent/tools/sdk.py b/WorkflowExecAgent/tools/sdk.py index e04d1e2f15..03f4dc74de 100644 --- a/WorkflowExecAgent/tools/sdk.py +++ b/WorkflowExecAgent/tools/sdk.py @@ -7,7 +7,7 @@ from tools.utils.handle_requests import RequestHandler -class EasyDataSDK: # Example SDK class for Data Insight Automation platform +class DataInsightAutomationSDK: # Example SDK class for Data Insight Automation platform """SDK class containing all components. Attributes: diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index 2e1e18ee03..6e38477ec1 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -3,7 +3,7 @@ import time -from tools.sdk import EasyDataSDK +from tools.sdk import DataInsightAutomationSDK def workflow_executor(params, workflow_id: int) -> dict: @@ -18,7 +18,7 @@ def workflow_executor(params, workflow_id: int) -> dict: # Replace function logic with use-case - sdk = EasyDataSDK() # Initialize SDK instance + sdk = DataInsightAutomationSDK() # Initialize SDK instance workflow = sdk.create_workflow(workflow_id) # Create workflow instance object params = {key: str(val) for key, val in params.items()} From 77675ce7c74beb3b9be6fffce22d675f55c4bc54 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Fri, 8 Nov 2024 04:26:35 +0000 Subject: [PATCH 15/21] Update readme for example workflow Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/README.md | 69 ++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/WorkflowExecAgent/README.md b/WorkflowExecAgent/README.md index 0a4b7f333e..03f6640612 100644 --- a/WorkflowExecAgent/README.md +++ b/WorkflowExecAgent/README.md @@ -4,13 +4,25 @@ GenAI Workflow Executor Example showcases the capability to handle data/AI workflow operations via LangChain agents to execute custom-defined workflow-based tools. These workflow tools can be interfaced from any 3rd-party tools in the market (no-code/low-code/IDE) such as Alteryx, RapidMiner, Power BI, Intel Data Insight Automation which allows users to create complex data/AI workflow operations for different use-cases. +### Definitions + +Before we begin, here are the definitions to some terms for clarity: + +- servable/serving workflow - A workflow made ready to be executed through API. It should be able to accept paramater injection for workflow scheduling and have a way to retrieve the final output data. It should also have a unique workflow ID for referencing. For platform providers guide to create their own servable workflows compatible with this example, refer to [Workflow Building Platform](#workflow-building-platform) + +- SDK Class - Performs requests to interface with a 3rd-party API to perform workflow operations on the servable workflow. Found in `tools/sdk.py`. + +- workflow ID - A unique ID for the servable workflow. + +- workflow instance - An instance created from the servable workflow. It is represented as a `Workflow` class created using `DataInsightAutomationSDK.create_workflow()` under `tools/sdk.py`. Contains methods to `start`, `get_status` and `get_results` from the workflow. + ### Workflow Executor -This example demonstrates a single React-LangGraph with a `Workflow Executor` tool to ingest a user prompt to execute workflows and return an agent reasoning response based on the workflow output data. +Strategy - This example demonstrates a single React-LangGraph with a `Workflow Executor` tool to ingest a user prompt to execute workflows and return an agent reasoning response based on the workflow output data. First the LLM extracts the relevant information from the user query based on the schema of the tool in `tools/tools.yaml`. Then the agent sends this `AgentState` to the `Workflow Executor` tool. -`Workflow Executor` tool uses `EasyDataSDK` class as seen under `tools/sdk.py` to interface with several high-level API's. There are 3 steps to this tool implementation: +`Workflow Executor` tool requires a SDK class to call the servable workflow API. In the code, `DataInsightAutomationSDK` is the example class as seen under `tools/sdk.py` to interface with several high-level API's. There are 3 steps to this tool implementation: 1. Starts the workflow with workflow parameters and workflow id extracted from the user query. @@ -26,19 +38,23 @@ Below shows an illustration of this flow: ### Workflow Serving for Agent +#### Workflow Building Platform + +The first step is to prepare a servable workflow using a platform with the capabilities to do so. + As an example, here we have a Churn Prediction use-case workflow as the serving workflow for the agent execution. It is created through Intel Data Insight Automation platform. The image below shows a snapshot of the Churn Prediction workflow. ![image](https://github.com/user-attachments/assets/c067f8b3-86cf-4abc-a8bd-51a98de8172d) -The workflow contains 2 paths which can be seen in the workflow illustrated, the top path and bottom path. +The workflow contains 2 paths which can be seen in the workflow illustrated, the top and bottom paths. -1. Top path - The training path which ends at the random forest classifier node is the training path. The data is cleaned through a series of nodes and used to train a random forest model for prediction. +1. Top path (Training path) - Ends at the random forest classifier node is the training path. The data is cleaned through a series of nodes and used to train a random forest model for prediction. -2. Bottom path - The inference path where trained random forest model is used for inferencing based on input parameter. +2. Bottom path (Inference path) - where trained random forest model is used for inferencing based on input parameter. For this agent workflow execution, the inferencing path is executed to yield the final output result of the `Model Predictor` node. The same output is returned to the `Workflow Executor` tool through the `Langchain API Serving` node. -There are `Serving Parameters` in the workflow, which are the tool input variables used to start a workflow instance obtained from `params` the LLM extracts from the user query. Below shows the parameter configuration option for the Intel Data Insight Automation workflow UI. +There are `Serving Parameters` in the workflow, which are the tool input variables used to start a workflow instance at runtime obtained from `params` the LLM extracts from the user query. Below shows the parameter configuration option for the Intel Data Insight Automation workflow UI. ![image](https://github.com/user-attachments/assets/ce8ef01a-56ff-4278-b84d-b6e4592b28c6) @@ -48,7 +64,16 @@ Manually running the workflow yields the tabular data output as shown below: In the workflow serving for agent, this output will be returned to the `Workflow Executor` tool. The LLM can then answer the user's original question based on this output. -To start prompting the agent microservice, we will use the following command for this use case: +When the workflow is configured as desired, transform this into a servable workflow. We turn this workflow into a servable workflow format so that it can be called through API to perform operations on it. Data Insight Automation has tools to do this for its own workflows. + +> [!NOTE] +> Remember to create a unique workflow ID along with the servable workflow. + +#### Using Servable Workflow + +Once we have our servable workflow ready, the serving workflow API can be prepared to accept requests from the SDK class. Refer to [Start Agent Microservice](#start-agent-microservice) on how to do this. + +To start prompting the agent microservice, we will use the following command for this churn prediction use-case: ```sh curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ @@ -56,7 +81,7 @@ curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: app }' ``` -The user has to provide a `workflow_id` and workflow `params` in the query. `workflow_id` a unique id used for serving the workflow to the microservice. Notice that the `query` string includes all the workflow `params` which the user has defined in the workflow. The LLM will extract these parameters into a dictionary format for the workflow `Serving Parameters` as shown below: +The user has to provide a `workflow_id` and workflow `params` in the query. Notice that the `query` string includes all the workflow `params` which the user has defined in the workflow. The LLM will extract these parameters into a dictionary format for the workflow `Serving Parameters` as shown below: ```python params = {"gender": "Female", "tenure": 55, "MonthlyAvgCharges": 103.7} @@ -72,6 +97,16 @@ And finally here are the results from the microservice logs: ### Start Agent Microservice +For an out-of-box experience there is an example workflow serving API service prepared for users under `tests/example_workflow` to interface with the SDK. This section will guide users on setting up this service as well. Users may modify the logic, add your own database etc for your own use-case. + +There are 3 services needed for the setup: + +1. Agent microservice + +2. LLM inference service - specified as `llm_endpoint_url`. + +3. workflow serving API service - specified as `SDK_BASE_URL` + Workflow Executor will have a single docker image. First, build the agent docker image. ```sh @@ -83,8 +118,9 @@ docker compose -f build.yaml build --no-cache Configure `GenAIExamples/WorkflowExecAgent/docker_compose/.env` file with the following. Replace the variables according to your usecase. ```sh -export SDK_BASE_URL=${SDK_BASE_URL} -export SERVING_TOKEN=${SERVING_TOKEN} +export wf_api_port=5000 # workflow serving API port to use +export SDK_BASE_URL=http://$(hostname -I | awk '{print $1}'):${wf_api_port}/ # The workflow server will use this example workflow API url +export SERVING_TOKEN=${SERVING_TOKEN} # For example_workflow, can be empty export HUGGINGFACEHUB_API_TOKEN=${HF_TOKEN} export llm_engine=${llm_engine} export llm_endpoint_url=${llm_endpoint_url} @@ -106,9 +142,18 @@ cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_compose docker compose -f compose.yaml up -d ``` +To launch the example workflow API server, open a new terminal and run the following: + +```sh +cd $WORKDIR/GenAIExamples/WorkflowExecAgent/tests/example_workflow +. launch_workflow_service.sh +``` + +`launch_workflow_service.sh` will setup all the packages locally and launch the uvicorn server to host the API on port 5000. For a Dockerfile method, please refer to `Dockerfile.example_workflow_api` file. + ### Validate service -The microservice logs can be viewed using: +The agent microservice logs can be viewed using: ```sh docker logs workflowexec-agent-endpoint @@ -120,7 +165,7 @@ You can validate the service using the following command: ```sh curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ - "query": "I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." + "query": "I have a data with gender Female, tenure 55, MonthlyCharges 103.7, TotalCharges 1840.75. Predict if this entry will churn. My workflow id is '${workflow_id}'." }' ``` From 37451427bda36e6a6883784c644c2607cca6febf Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Fri, 8 Nov 2024 09:16:31 +0000 Subject: [PATCH 16/21] Update test scripts and readme Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/README.md | 2 +- WorkflowExecAgent/tests/1_build_images.sh | 2 +- WorkflowExecAgent/tests/3_launch_agent_service.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WorkflowExecAgent/README.md b/WorkflowExecAgent/README.md index 03f6640612..19928fc813 100644 --- a/WorkflowExecAgent/README.md +++ b/WorkflowExecAgent/README.md @@ -56,7 +56,7 @@ For this agent workflow execution, the inferencing path is executed to yield the There are `Serving Parameters` in the workflow, which are the tool input variables used to start a workflow instance at runtime obtained from `params` the LLM extracts from the user query. Below shows the parameter configuration option for the Intel Data Insight Automation workflow UI. -![image](https://github.com/user-attachments/assets/ce8ef01a-56ff-4278-b84d-b6e4592b28c6) +image Manually running the workflow yields the tabular data output as shown below: diff --git a/WorkflowExecAgent/tests/1_build_images.sh b/WorkflowExecAgent/tests/1_build_images.sh index 85ea4ab097..ebb4883f44 100644 --- a/WorkflowExecAgent/tests/1_build_images.sh +++ b/WorkflowExecAgent/tests/1_build_images.sh @@ -17,7 +17,7 @@ function build_agent_docker_image() { cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_image_build/ get_genai_comps echo "Build agent image with --no-cache..." - docker compose -f build.yaml build + docker compose -f build.yaml build --no-cache } function main() { diff --git a/WorkflowExecAgent/tests/3_launch_agent_service.sh b/WorkflowExecAgent/tests/3_launch_agent_service.sh index 0d7f53da6f..5aca6590d9 100644 --- a/WorkflowExecAgent/tests/3_launch_agent_service.sh +++ b/WorkflowExecAgent/tests/3_launch_agent_service.sh @@ -5,7 +5,6 @@ set -e WORKPATH=$(dirname "$PWD") -workflow_id=9809 vllm_port=${vllm_port} [[ -z "$vllm_port" ]] && vllm_port=8084 export WORKDIR=$WORKPATH/../../ @@ -21,6 +20,7 @@ export recursion_limit=25 export temperature=0 export max_new_tokens=1000 export TOOLSET_PATH=$WORKDIR/GenAIExamples/WorkflowExecAgent/tools/ +export workflow_id=9838 function start_agent() { echo "Starting Agent services" From 07d4ed4dc5ec9c5d488f33ca98a13834b724d767 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:21:30 +0000 Subject: [PATCH 17/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- WorkflowExecAgent/README.md | 4 +- .../tests/3_launch_agent_service.sh | 2 +- .../tests/3_launch_example_wf_api.sh | 2 +- .../launch_workflow_service.sh | 3 + .../tests/example_workflow/main.py | 27 ++-- .../tests/example_workflow/workflow.py | 117 ++++++++++-------- .../tools/components/component.py | 4 +- .../tools/components/workflow.py | 2 +- WorkflowExecAgent/tools/sdk.py | 8 +- WorkflowExecAgent/tools/tools.py | 4 +- 10 files changed, 99 insertions(+), 74 deletions(-) diff --git a/WorkflowExecAgent/README.md b/WorkflowExecAgent/README.md index 19928fc813..7bc56cc3d9 100644 --- a/WorkflowExecAgent/README.md +++ b/WorkflowExecAgent/README.md @@ -5,10 +5,10 @@ GenAI Workflow Executor Example showcases the capability to handle data/AI workflow operations via LangChain agents to execute custom-defined workflow-based tools. These workflow tools can be interfaced from any 3rd-party tools in the market (no-code/low-code/IDE) such as Alteryx, RapidMiner, Power BI, Intel Data Insight Automation which allows users to create complex data/AI workflow operations for different use-cases. ### Definitions - + Before we begin, here are the definitions to some terms for clarity: -- servable/serving workflow - A workflow made ready to be executed through API. It should be able to accept paramater injection for workflow scheduling and have a way to retrieve the final output data. It should also have a unique workflow ID for referencing. For platform providers guide to create their own servable workflows compatible with this example, refer to [Workflow Building Platform](#workflow-building-platform) +- servable/serving workflow - A workflow made ready to be executed through API. It should be able to accept parameter injection for workflow scheduling and have a way to retrieve the final output data. It should also have a unique workflow ID for referencing. For platform providers guide to create their own servable workflows compatible with this example, refer to [Workflow Building Platform](#workflow-building-platform) - SDK Class - Performs requests to interface with a 3rd-party API to perform workflow operations on the servable workflow. Found in `tools/sdk.py`. diff --git a/WorkflowExecAgent/tests/3_launch_agent_service.sh b/WorkflowExecAgent/tests/3_launch_agent_service.sh index 5aca6590d9..97706a0c8a 100644 --- a/WorkflowExecAgent/tests/3_launch_agent_service.sh +++ b/WorkflowExecAgent/tests/3_launch_agent_service.sh @@ -33,7 +33,7 @@ function start_agent() { function main() { echo "==================== Start agent service ====================" start_agent - echo "==================== Agent service started ====================" + echo "==================== Agent service started ====================" } main diff --git a/WorkflowExecAgent/tests/3_launch_example_wf_api.sh b/WorkflowExecAgent/tests/3_launch_example_wf_api.sh index 8d736c2935..5096830e47 100644 --- a/WorkflowExecAgent/tests/3_launch_example_wf_api.sh +++ b/WorkflowExecAgent/tests/3_launch_example_wf_api.sh @@ -34,7 +34,7 @@ function start_example_workflow_api() { function main() { echo "==================== Start example workflow API ====================" start_example_workflow_api - echo "==================== Example workflow API started ====================" + echo "==================== Example workflow API started ====================" } main diff --git a/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh b/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh index 9367aaf28d..3bf50335f6 100644 --- a/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh +++ b/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + apt-get -qq -y install --no-install-recommends unzip curl ca-certificates apt-get -qq -y install --no-install-recommends python3 python3-pip diff --git a/WorkflowExecAgent/tests/example_workflow/main.py b/WorkflowExecAgent/tests/example_workflow/main.py index 81ac51e252..6d9fe9510b 100644 --- a/WorkflowExecAgent/tests/example_workflow/main.py +++ b/WorkflowExecAgent/tests/example_workflow/main.py @@ -1,22 +1,22 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + import logging -from fastapi import FastAPI, APIRouter +from fastapi import APIRouter, FastAPI from workflow import run_workflow - logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) app = FastAPI() -router = APIRouter( - prefix="/serving", - tags=["Workflow Serving"] -) +router = APIRouter(prefix="/serving", tags=["Workflow Serving"]) app.results = {} -@router.post('/servable_workflows/{wf_id}/start', summary="Start Workflow") + +@router.post("/servable_workflows/{wf_id}/start", summary="Start Workflow") async def start_workflow(wf_id: int, params: dict): try: app.results = run_workflow(params["params"]) @@ -25,9 +25,10 @@ async def start_workflow(wf_id: int, params: dict): except Exception as e: logging.error(e, exc_info=True) - return {"msg": "error occured"} - -@router.get('/serving_workflows/{wf_key}/status', summary="Get Workflow Status") + return {"msg": "error occurred"} + + +@router.get("/serving_workflows/{wf_key}/status", summary="Get Workflow Status") async def get_status(wf_key: str): try: if app.results: @@ -38,9 +39,10 @@ async def get_status(wf_key: str): return {"workflow_status": status} except Exception as e: logging.error(e) - return {"msg": "error occured"} + return {"msg": "error occurred"} + -@router.get('/serving_workflows/{wf_key}/results', summary = "Get Workflow Results") +@router.get("/serving_workflows/{wf_key}/results", summary="Get Workflow Results") async def get_results(wf_key: str): try: if app.results: @@ -52,4 +54,5 @@ async def get_results(wf_key: str): logging.error(e) return {"msg": "There is an issue while getting results !!"} + app.include_router(router) diff --git a/WorkflowExecAgent/tests/example_workflow/workflow.py b/WorkflowExecAgent/tests/example_workflow/workflow.py index 5697a33248..f5609c2b4d 100644 --- a/WorkflowExecAgent/tests/example_workflow/workflow.py +++ b/WorkflowExecAgent/tests/example_workflow/workflow.py @@ -1,74 +1,95 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + import json import warnings -warnings.filterwarnings('ignore') + +warnings.filterwarnings("ignore") import pandas as pd -from sklearn.preprocessing import MinMaxScaler, LabelEncoder -from sklearn.tree import DecisionTreeClassifier -from sklearn.naive_bayes import GaussianNB -from sklearn.neighbors import KNeighborsClassifier -from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier, GradientBoostingClassifier -from sklearn.svm import SVC +from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier, RandomForestClassifier from sklearn.linear_model import LogisticRegression -from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.metrics import accuracy_score +from sklearn.model_selection import GridSearchCV, train_test_split +from sklearn.naive_bayes import GaussianNB +from sklearn.neighbors import KNeighborsClassifier from sklearn.pipeline import Pipeline +from sklearn.preprocessing import LabelEncoder, MinMaxScaler +from sklearn.svm import SVC +from sklearn.tree import DecisionTreeClassifier from xgboost import XGBClassifier def churn_prediction(params): - df = pd.read_csv('./WA_Fn-UseC_-Telco-Customer-Churn.csv') + df = pd.read_csv("./WA_Fn-UseC_-Telco-Customer-Churn.csv") # Data Cleaning - df = df.drop(['customerID'], axis=1) - select_cols = ["gender", "tenure", "MonthlyCharges", "TotalCharges", "Churn"] + df = df.drop(["customerID"], axis=1) + select_cols = ["gender", "tenure", "MonthlyCharges", "TotalCharges", "Churn"] df = df[select_cols] - df['TotalCharges'] = pd.to_numeric(df.TotalCharges, errors='coerce') - df.drop(labels=df[df['tenure'] == 0].index, axis=0, inplace=True) + df["TotalCharges"] = pd.to_numeric(df.TotalCharges, errors="coerce") + df.drop(labels=df[df["tenure"] == 0].index, axis=0, inplace=True) df.fillna(df["TotalCharges"].mean()) # Data Preprocessing encoders = {} + def object_to_int(dataframe_series): - if dataframe_series.dtype=='object': + if dataframe_series.dtype == "object": encoders[dataframe_series.name] = LabelEncoder().fit(dataframe_series) dataframe_series = encoders[dataframe_series.name].transform(dataframe_series) return dataframe_series - + df = df.apply(lambda x: object_to_int(x)) - X = df.drop(columns=['Churn']) - y = df['Churn'].values + X = df.drop(columns=["Churn"]) + y = df["Churn"].values X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=40, stratify=y) - + model_scores = [] models = [ - ('Random Forest', RandomForestClassifier(random_state=42), - {'model__n_estimators': [50, 100, 200], - 'model__max_depth': [None, 10, 20]}), # Add hyperparameters for Random Forest - ('Gradient Boosting', GradientBoostingClassifier(random_state=42), - {'model__n_estimators': [50, 100, 200], - 'model__learning_rate': [0.05, 0.1, 0.5]}), # Add hyperparameters for Gradient Boosting - ('Support Vector Machine', SVC(random_state=42, class_weight='balanced'), - {'model__C': [0.1, 1, 10], - 'model__gamma': ['scale', 'auto']}), # Add hyperparameters for SVM - ('Logistic Regression', LogisticRegression(random_state=42, class_weight='balanced'), - {'model__C': [0.1, 1, 10], - 'model__penalty': ['l1', 'l2']}), # Add hyperparameters for Logistic Regression - ('K-Nearest Neighbors', KNeighborsClassifier(), - {'model__n_neighbors': [3, 5, 7], - 'model__weights': ['uniform', 'distance']}), # Add hyperparameters for KNN - ('Decision Tree', DecisionTreeClassifier(random_state=42), - {'model__max_depth': [None, 10, 20], - 'model__min_samples_split': [2, 5, 10]}), # Add hyperparameters for Decision Tree - ('Ada Boost', AdaBoostClassifier(random_state=42), - {'model__n_estimators': [50, 100, 200], - 'model__learning_rate': [0.05, 0.1, 0.5]}), # Add hyperparameters for Ada Boost - ('XG Boost', XGBClassifier(random_state=42), - {'model__n_estimators': [50, 100, 200], - 'model__learning_rate': [0.05, 0.1, 0.5]}), # Add hyperparameters for XG Boost - ('Naive Bayes', GaussianNB(), {}) # No hyperparameters for Naive Bayes + ( + "Random Forest", + RandomForestClassifier(random_state=42), + {"model__n_estimators": [50, 100, 200], "model__max_depth": [None, 10, 20]}, + ), # Add hyperparameters for Random Forest + ( + "Gradient Boosting", + GradientBoostingClassifier(random_state=42), + {"model__n_estimators": [50, 100, 200], "model__learning_rate": [0.05, 0.1, 0.5]}, + ), # Add hyperparameters for Gradient Boosting + ( + "Support Vector Machine", + SVC(random_state=42, class_weight="balanced"), + {"model__C": [0.1, 1, 10], "model__gamma": ["scale", "auto"]}, + ), # Add hyperparameters for SVM + ( + "Logistic Regression", + LogisticRegression(random_state=42, class_weight="balanced"), + {"model__C": [0.1, 1, 10], "model__penalty": ["l1", "l2"]}, + ), # Add hyperparameters for Logistic Regression + ( + "K-Nearest Neighbors", + KNeighborsClassifier(), + {"model__n_neighbors": [3, 5, 7], "model__weights": ["uniform", "distance"]}, + ), # Add hyperparameters for KNN + ( + "Decision Tree", + DecisionTreeClassifier(random_state=42), + {"model__max_depth": [None, 10, 20], "model__min_samples_split": [2, 5, 10]}, + ), # Add hyperparameters for Decision Tree + ( + "Ada Boost", + AdaBoostClassifier(random_state=42), + {"model__n_estimators": [50, 100, 200], "model__learning_rate": [0.05, 0.1, 0.5]}, + ), # Add hyperparameters for Ada Boost + ( + "XG Boost", + XGBClassifier(random_state=42), + {"model__n_estimators": [50, 100, 200], "model__learning_rate": [0.05, 0.1, 0.5]}, + ), # Add hyperparameters for XG Boost + ("Naive Bayes", GaussianNB(), {}), # No hyperparameters for Naive Bayes ] best_model = None @@ -76,10 +97,7 @@ def object_to_int(dataframe_series): for name, model, param_grid in models: # Create a pipeline for each model - pipeline = Pipeline([ - ('scaler', MinMaxScaler()), # Feature Scaling - ('model', model) - ]) + pipeline = Pipeline([("scaler", MinMaxScaler()), ("model", model)]) # Feature Scaling # Hyperparameter tuning using GridSearchCV if param_grid: @@ -97,12 +115,12 @@ def object_to_int(dataframe_series): accuracy = accuracy_score(y_test, y_pred) # Append model name and accuracy to the list - model_scores.append({'Model': name, 'Accuracy': accuracy}) + model_scores.append({"Model": name, "Accuracy": accuracy}) # Convert the list to a DataFrame scores_df = pd.DataFrame(model_scores) print("Model:", name) - print("Test Accuracy:", round(accuracy, 3),"%\n") + print("Test Accuracy:", round(accuracy, 3), "%\n") # Check if the current model has the best accuracy if accuracy > best_accuracy: @@ -133,5 +151,6 @@ def transform_params(name, value): return result + def run_workflow(params): return churn_prediction(params) diff --git a/WorkflowExecAgent/tools/components/component.py b/WorkflowExecAgent/tools/components/component.py index b2897c18b1..391109eaad 100644 --- a/WorkflowExecAgent/tools/components/component.py +++ b/WorkflowExecAgent/tools/components/component.py @@ -14,8 +14,8 @@ def __init__(self, request_handler): def _make_request(self, *args, **kwargs): """Uses the request_handler object to make API requests. - + :returns: API response """ - + return self.request_handler._make_request(*args, **kwargs) diff --git a/WorkflowExecAgent/tools/components/workflow.py b/WorkflowExecAgent/tools/components/workflow.py index e68218b7fd..9c48ffe996 100644 --- a/WorkflowExecAgent/tools/components/workflow.py +++ b/WorkflowExecAgent/tools/components/workflow.py @@ -33,7 +33,7 @@ def start(self, params: Dict[str, str]) -> Dict[str, str]: :rtype: string """ - + data = json.dumps({"params": params}) endpoint = f"serving/servable_workflows/{self.workflow_id}/start" self.wf_key = self._make_request(endpoint, "POST", data).get("wf_key", None) diff --git a/WorkflowExecAgent/tools/sdk.py b/WorkflowExecAgent/tools/sdk.py index 03f4dc74de..7b020edf1f 100644 --- a/WorkflowExecAgent/tools/sdk.py +++ b/WorkflowExecAgent/tools/sdk.py @@ -7,7 +7,7 @@ from tools.utils.handle_requests import RequestHandler -class DataInsightAutomationSDK: # Example SDK class for Data Insight Automation platform +class DataInsightAutomationSDK: # Example SDK class for Data Insight Automation platform """SDK class containing all components. Attributes: @@ -17,11 +17,11 @@ class DataInsightAutomationSDK: # Example SDK class for Data Insight Automa def __init__(self): self.request_handler = RequestHandler(os.environ["SDK_BASE_URL"], os.environ["SERVING_TOKEN"]) - def create_workflow(self, workflow_id:int=None, workflow_key=None): + def create_workflow(self, workflow_id: int = None, workflow_key=None): """Creates a Workflow object. - + :param int workflow_id: Servable workflow id. - + :returns: Workflow """ diff --git a/WorkflowExecAgent/tools/tools.py b/WorkflowExecAgent/tools/tools.py index 6e38477ec1..c51ae7bcc9 100644 --- a/WorkflowExecAgent/tools/tools.py +++ b/WorkflowExecAgent/tools/tools.py @@ -18,8 +18,8 @@ def workflow_executor(params, workflow_id: int) -> dict: # Replace function logic with use-case - sdk = DataInsightAutomationSDK() # Initialize SDK instance - workflow = sdk.create_workflow(workflow_id) # Create workflow instance object + sdk = DataInsightAutomationSDK() # Initialize SDK instance + workflow = sdk.create_workflow(workflow_id) # Create workflow instance object params = {key: str(val) for key, val in params.items()} start_workflow = workflow.start(params) From 683654b9989234907815f35a605eb620896797a9 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Fri, 8 Nov 2024 10:37:45 +0000 Subject: [PATCH 18/21] Update workflow id Signed-off-by: JoshuaL3000 --- .../tests/3_launch_agent_service.sh | 1 - .../tests/3_launch_and_validate_agent.sh | 66 ------------------- ...> test_compose_vllm_example_wf_on_xeon.sh} | 1 + .../tests/test_compose_vllm_on_xeon.sh | 1 + 4 files changed, 2 insertions(+), 67 deletions(-) delete mode 100644 WorkflowExecAgent/tests/3_launch_and_validate_agent.sh rename WorkflowExecAgent/tests/{test_compose_vllm_example_wf_xeon.sh => test_compose_vllm_example_wf_on_xeon.sh} (99%) diff --git a/WorkflowExecAgent/tests/3_launch_agent_service.sh b/WorkflowExecAgent/tests/3_launch_agent_service.sh index 97706a0c8a..874f1f5b84 100644 --- a/WorkflowExecAgent/tests/3_launch_agent_service.sh +++ b/WorkflowExecAgent/tests/3_launch_agent_service.sh @@ -20,7 +20,6 @@ export recursion_limit=25 export temperature=0 export max_new_tokens=1000 export TOOLSET_PATH=$WORKDIR/GenAIExamples/WorkflowExecAgent/tools/ -export workflow_id=9838 function start_agent() { echo "Starting Agent services" diff --git a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh b/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh deleted file mode 100644 index 5c9e6da583..0000000000 --- a/WorkflowExecAgent/tests/3_launch_and_validate_agent.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - -set -e - -WORKPATH=$(dirname "$PWD") -workflow_id=9809 -vllm_port=${vllm_port} -[[ -z "$vllm_port" ]] && vllm_port=8084 -export WORKDIR=$WORKPATH/../../ -echo "WORKDIR=${WORKDIR}" -export SDK_BASE_URL=${SDK_BASE_URL} -export SERVING_TOKEN=${SERVING_TOKEN} -export HF_TOKEN=${HUGGINGFACEHUB_API_TOKEN} -export llm_engine=vllm -export ip_address=$(hostname -I | awk '{print $1}') -export llm_endpoint_url=http://${ip_address}:${vllm_port} -export model=mistralai/Mistral-7B-Instruct-v0.3 -export recursion_limit=25 -export temperature=0 -export max_new_tokens=1000 -export TOOLSET_PATH=$WORKDIR/GenAIExamples/WorkflowExecAgent/tools/ - -function start_agent_and_api_server() { - echo "Starting Agent services" - cd $WORKDIR/GenAIExamples/WorkflowExecAgent/docker_compose/intel/cpu/xeon - WORKDIR=$WORKPATH/docker_image_build/ docker compose -f compose_vllm.yaml up -d - echo "Waiting agent service ready" - sleep 5s -} - -function validate() { - local CONTENT="$1" - local EXPECTED_RESULT="$2" - local SERVICE_NAME="$3" - - if echo "$CONTENT" | grep -q "$EXPECTED_RESULT"; then - echo "[ $SERVICE_NAME ] Content is as expected: $CONTENT" - echo "[TEST INFO]: Workflow Executor agent service PASSED" - else - echo "[ $SERVICE_NAME ] Content does not match the expected result: $CONTENT" - echo "[TEST INFO]: Workflow Executor agent service FAILED" - fi -} - -function validate_agent_service() { - echo "----------------Test agent ----------------" - local CONTENT=$(curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ - "query": "I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." - }') - validate "$CONTENT" "The entry is not likely to churn" "workflowexec-agent-endpoint" - docker logs workflowexec-agent-endpoint -} - -function main() { - echo "==================== Start agent ====================" - start_agent_and_api_server - echo "==================== Agent started ====================" - - echo "==================== Validate agent service ====================" - validate_agent_service - echo "==================== Agent service validated ====================" -} - -main diff --git a/WorkflowExecAgent/tests/test_compose_vllm_example_wf_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh similarity index 99% rename from WorkflowExecAgent/tests/test_compose_vllm_example_wf_xeon.sh rename to WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh index 82528548c0..561b025543 100644 --- a/WorkflowExecAgent/tests/test_compose_vllm_example_wf_xeon.sh +++ b/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh @@ -5,6 +5,7 @@ wf_api_port=${wf_api_port} [[ -z "$wf_api_port" ]] && wf_api_port=5000 api_server_url=http://$(hostname -I | awk '{print $1}'):${wf_api_port}/ +workflow_id=9838 query="I have a data with gender Female, tenure 55, MonthlyCharges 103.7, TotalCharges 1840.75. Predict if this entry will churn. My workflow id is ${workflow_id}." validate_result="the prediction is No" diff --git a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh index a316fe0734..a42d7bd751 100644 --- a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh +++ b/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh @@ -2,6 +2,7 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +workflow_id=9838 query="I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." validate_result="The entry is not likely to churn" From 0523b683967a009a05556f9d81c6361ee62df08d Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Thu, 16 Jan 2025 04:36:14 +0000 Subject: [PATCH 19/21] Update docker compose path for agent comps change Signed-off-by: JoshuaL3000 --- .../docker_compose/intel/cpu/xeon/compose_vllm.yaml | 2 +- WorkflowExecAgent/tests/2_start_vllm_service.sh | 2 +- WorkflowExecAgent/tests/README.md | 2 +- WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh | 2 +- WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml b/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml index 4304831f27..3aa2c6d5f5 100644 --- a/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml +++ b/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml @@ -6,7 +6,7 @@ services: image: ${REGISTRY:-opea}/agent-langchain:${TAG:-latest} container_name: workflowexec-agent-endpoint volumes: - - ${WORKDIR}/GenAIComps/comps/agent/langchain/:/home/user/comps/agent/langchain/ + - ${WORKDIR}/GenAIComps/comps/agent/src/:/home/user/comps/agent/src/ - ${TOOLSET_PATH}:/home/user/tools/ ports: - "9090:9090" diff --git a/WorkflowExecAgent/tests/2_start_vllm_service.sh b/WorkflowExecAgent/tests/2_start_vllm_service.sh index 2c34253284..6f2d428421 100644 --- a/WorkflowExecAgent/tests/2_start_vllm_service.sh +++ b/WorkflowExecAgent/tests/2_start_vllm_service.sh @@ -18,7 +18,7 @@ function build_vllm_docker_image() { echo $WORKPATH if [ ! -d "./vllm" ]; then git clone https://github.com/vllm-project/vllm.git - cd ./vllm; git checkout tags/v0.6.0 + cd ./vllm; git checkout tags/v0.6.6 else cd ./vllm fi diff --git a/WorkflowExecAgent/tests/README.md b/WorkflowExecAgent/tests/README.md index 1dbaab6e93..0ea9504a5d 100644 --- a/WorkflowExecAgent/tests/README.md +++ b/WorkflowExecAgent/tests/README.md @@ -24,7 +24,7 @@ Launch validation by running the following command. ```sh cd GenAIExamples/WorkflowExecAgent/tests -. /test_compose_on_xeon.sh +. /test_compose_vllm_on_xeon.sh ``` `test_compose_on_xeon.sh` will run the other `.sh` files under `tests/`. The validation script launches 1 docker container for the agent microservice, and another for the vllm model serving on CPU. When validation is completed, all containers will be stopped. diff --git a/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh index 561b025543..febf6e13ef 100644 --- a/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh +++ b/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh @@ -5,7 +5,7 @@ wf_api_port=${wf_api_port} [[ -z "$wf_api_port" ]] && wf_api_port=5000 api_server_url=http://$(hostname -I | awk '{print $1}'):${wf_api_port}/ -workflow_id=9838 +workflow_id=10071 query="I have a data with gender Female, tenure 55, MonthlyCharges 103.7, TotalCharges 1840.75. Predict if this entry will churn. My workflow id is ${workflow_id}." validate_result="the prediction is No" diff --git a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh index a42d7bd751..76237b51c1 100644 --- a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh +++ b/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh @@ -2,7 +2,7 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -workflow_id=9838 +workflow_id=10071 query="I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." validate_result="The entry is not likely to churn" From 8bb3f30e55edc1cdd82291ad8495d4855960ef24 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Fri, 11 Apr 2025 08:52:29 +0000 Subject: [PATCH 20/21] Enable test for workflow example API after package upgrades Signed-off-by: JoshuaL3000 --- .../docker_compose/intel/cpu/xeon/compose_vllm.yaml | 2 +- WorkflowExecAgent/tests/3_launch_example_wf_api.sh | 2 +- WorkflowExecAgent/tests/4_validate_agent.sh | 4 ++-- .../{test_compose_vllm_on_xeon.sh => compose_vllm_on_xeon.sh} | 2 +- .../tests/example_workflow/launch_workflow_service.sh | 2 +- WorkflowExecAgent/tools/custom_prompt.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename WorkflowExecAgent/tests/{test_compose_vllm_on_xeon.sh => compose_vllm_on_xeon.sh} (98%) diff --git a/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml b/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml index e69e9ef7f2..44a2b365d0 100644 --- a/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml +++ b/WorkflowExecAgent/docker_compose/intel/cpu/xeon/compose_vllm.yaml @@ -9,7 +9,7 @@ services: - ${WORKDIR}/GenAIComps/comps/agent/src/:/home/user/comps/agent/src/ - ${TOOLSET_PATH}:/home/user/tools/ ports: - - "9090:9090" + - "9091:9090" ipc: host environment: ip_address: ${ip_address} diff --git a/WorkflowExecAgent/tests/3_launch_example_wf_api.sh b/WorkflowExecAgent/tests/3_launch_example_wf_api.sh index 5096830e47..7b4d776df7 100644 --- a/WorkflowExecAgent/tests/3_launch_example_wf_api.sh +++ b/WorkflowExecAgent/tests/3_launch_example_wf_api.sh @@ -5,7 +5,7 @@ set -e wf_api_port=${wf_api_port} -[[ -z "$wf_api_port" ]] && wf_api_port=5000 +[[ -z "$wf_api_port" ]] && wf_api_port=5005 WORKPATH=$(dirname "$PWD") LOG_PATH="$WORKPATH/tests/example_workflow" export WORKDIR=$WORKPATH/../../ diff --git a/WorkflowExecAgent/tests/4_validate_agent.sh b/WorkflowExecAgent/tests/4_validate_agent.sh index ad9e9342c9..1d58640cfc 100644 --- a/WorkflowExecAgent/tests/4_validate_agent.sh +++ b/WorkflowExecAgent/tests/4_validate_agent.sh @@ -27,8 +27,8 @@ function validate() { function validate_agent_service() { echo "----------------Test agent ----------------" - local CONTENT=$(curl http://${ip_address}:9090/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ - "query": "'"${query}"'" + local CONTENT=$(curl http://${ip_address}:9091/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{ + "messages": "'"${query}"'" }') validate "$CONTENT" "$validate_result" "workflowexec-agent-endpoint" docker logs workflowexec-agent-endpoint diff --git a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh b/WorkflowExecAgent/tests/compose_vllm_on_xeon.sh similarity index 98% rename from WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh rename to WorkflowExecAgent/tests/compose_vllm_on_xeon.sh index 76237b51c1..cd59bedae9 100644 --- a/WorkflowExecAgent/tests/test_compose_vllm_on_xeon.sh +++ b/WorkflowExecAgent/tests/compose_vllm_on_xeon.sh @@ -2,7 +2,7 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -workflow_id=10071 +workflow_id=10277 query="I have a data with gender Female, tenure 55, MonthlyAvgCharges 103.7. Predict if this entry will churn. My workflow id is '${workflow_id}'." validate_result="The entry is not likely to churn" diff --git a/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh b/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh index 3bf50335f6..a006733891 100644 --- a/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh +++ b/WorkflowExecAgent/tests/example_workflow/launch_workflow_service.sh @@ -15,4 +15,4 @@ pip install virtualenv && \ source venv/bin/activate && \ pip install -r requirements.txt -uvicorn main:app --reload --port=5000 --host=0.0.0.0 +uvicorn main:app --reload --port=5005 --host=0.0.0.0 diff --git a/WorkflowExecAgent/tools/custom_prompt.py b/WorkflowExecAgent/tools/custom_prompt.py index bdad5d6b92..56bf88fb59 100644 --- a/WorkflowExecAgent/tools/custom_prompt.py +++ b/WorkflowExecAgent/tools/custom_prompt.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 REACT_SYS_MESSAGE = """\ -You are a helpful assistant. You are to start the workflow using the tool provided. +You are a helpful assistant. You are to start the workflow using the tool provided. Output tool calls or your answer at the end. After the workflow is completed, you will use the output data to answer the user's original question in a one short sentence. Now begin! From 219cab2352171e74dab9644d855b3865ee6dced4 Mon Sep 17 00:00:00 2001 From: JoshuaL3000 Date: Fri, 11 Apr 2025 09:17:52 +0000 Subject: [PATCH 21/21] Update wf_api_port Signed-off-by: JoshuaL3000 --- WorkflowExecAgent/tests/3_launch_agent_service.sh | 1 + WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/WorkflowExecAgent/tests/3_launch_agent_service.sh b/WorkflowExecAgent/tests/3_launch_agent_service.sh index 874f1f5b84..233305a4e8 100644 --- a/WorkflowExecAgent/tests/3_launch_agent_service.sh +++ b/WorkflowExecAgent/tests/3_launch_agent_service.sh @@ -10,6 +10,7 @@ vllm_port=${vllm_port} export WORKDIR=$WORKPATH/../../ echo "WORKDIR=${WORKDIR}" export SDK_BASE_URL=$1 +echo "SDK_BASE_URL=$1" export SERVING_TOKEN=${SERVING_TOKEN} export HF_TOKEN=${HUGGINGFACEHUB_API_TOKEN} export llm_engine=vllm diff --git a/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh b/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh index febf6e13ef..ca66058a8b 100644 --- a/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh +++ b/WorkflowExecAgent/tests/test_compose_vllm_example_wf_on_xeon.sh @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 wf_api_port=${wf_api_port} -[[ -z "$wf_api_port" ]] && wf_api_port=5000 +[[ -z "$wf_api_port" ]] && wf_api_port=5005 && export wf_api_port=5005 api_server_url=http://$(hostname -I | awk '{print $1}'):${wf_api_port}/ workflow_id=10071 query="I have a data with gender Female, tenure 55, MonthlyCharges 103.7, TotalCharges 1840.75. Predict if this entry will churn. My workflow id is ${workflow_id}."