From 539253125b65c23a13f5f7f067ad41c14ea40695 Mon Sep 17 00:00:00 2001 From: codebane Date: Thu, 5 Oct 2023 20:41:20 +0300 Subject: [PATCH 1/7] custom planner implementation with feedback --- llm-server/routes/root_service.py | 4 +- .../workflow/extractors/extract_param.py | 30 ++++----------- .../extractors/transform_api_response.py | 38 +++++++++++++++++++ .../workflow/generate_openapi_payload.py | 29 +++++--------- .../routes/workflow/workflow_controller.py | 19 +--------- .../routes/workflow/workflow_service.py | 37 ++++++++++++------ llm-server/utils/vector_db/add_workflow.py | 24 ++++++++++++ 7 files changed, 108 insertions(+), 73 deletions(-) create mode 100644 llm-server/routes/workflow/extractors/transform_api_response.py create mode 100644 llm-server/utils/vector_db/add_workflow.py diff --git a/llm-server/routes/root_service.py b/llm-server/routes/root_service.py index 138d7f3eb..b32e496f3 100644 --- a/llm-server/routes/root_service.py +++ b/llm-server/routes/root_service.py @@ -125,9 +125,7 @@ def handle_request(data: Dict[str, Any]) -> Any: logging.info( "[OpenCopilot] The user request can be handled in single API call" ) - raise "Falling back to planner" - # else: - # return {"": k} + except Exception as e: logging.info( "[OpenCopilot] Something went wrong when try to get how many calls is required" diff --git a/llm-server/routes/workflow/extractors/extract_param.py b/llm-server/routes/workflow/extractors/extract_param.py index 985df9c7b..d168bf7e7 100644 --- a/llm-server/routes/workflow/extractors/extract_param.py +++ b/llm-server/routes/workflow/extractors/extract_param.py @@ -13,28 +13,14 @@ def gen_params_from_schema( param_schema: str, text: str, prev_resp: str ) -> Optional[JsonData]: - """Extracts API parameters from a schema based on user text and previous response. - - Args: - param_schema (JsonData): A snippet of the OpenAPI parameter schema relevant to this operation. - text (str): The original user text query. - prev_resp (str): The previous API response. - - Returns: - Optional[JsonData]: The extracted JSON parameters, if successful. - - This function constructs a prompt with the given inputs and passes it to - an LLM to generate a JSON string containing the parameters. It then parses - this to extract a JSON payload matching the schema structure. - """ - - _DEFAULT_TEMPLATE = """In order to facilitate the sequential execution of a highly intelligent language model with a series of APIs, we furnish the vital information required for executing the next API call. - - The initial input at the onset of the process: {text} - The responses obtained from previous API calls: {prev_resp} - A schema for request parameters that defines the expected format: {param_schema} - - The JSON payload, which is used to represent the query parameters and is constructed using the initial input and previous API responses, must be enclosed within triple backticks on both sides. It must strictly adhere to the specified "type/format" guidelines laid out in the schema, and the structure is as follows:""" + + _DEFAULT_TEMPLATE = """We have the following information required for executing the next API call.\n + The initial input at the onset of the process: `{text}`\n + The responses obtained from previous API calls: ```{prev_resp}``` + Swagger Schema defining parameters: ```{param_schema}``` + Output musts be a json object representing the query params and nothing else. Also query params should only have keys defined in the swagger schema ignore anything else, moreover if user instruction doesnot have information about a query parameter ignore that as well. + + Query params: """ PROMPT = PromptTemplate( input_variables=["prev_resp", "text", "param_schema"], diff --git a/llm-server/routes/workflow/extractors/transform_api_response.py b/llm-server/routes/workflow/extractors/transform_api_response.py new file mode 100644 index 000000000..82864f0bb --- /dev/null +++ b/llm-server/routes/workflow/extractors/transform_api_response.py @@ -0,0 +1,38 @@ +import os +from langchain.prompts import PromptTemplate +from langchain.chains import LLMChain +from routes.workflow.extractors.extract_json import extract_json_payload +from utils.get_llm import get_llm +from custom_types.t_json import JsonData +from typing import Optional + +openai_api_key = os.getenv("OPENAI_API_KEY") +llm = get_llm() + + +def transform_api_response_from_schema( + server_url: str, api_response: str +) -> Optional[JsonData]: + _DEFAULT_TEMPLATE = """Given the following response from an api call```{api_response}``` extract the parts of information necessary for making furthur api calls to a backend server. Just return a json payload wrapped between three back ticks. use descriptive keys when returning the json""" + + PROMPT = PromptTemplate( + input_variables=["prev_resp", "text", "param_schema"], + template=_DEFAULT_TEMPLATE, + ) + + PROMPT.format( + api_response=api_response, + ) + + chain = LLMChain(llm=llm, prompt=PROMPT, verbose=True) + json_string = chain.run( + { + "api_response": api_response, + } + ) + + response = extract_json_payload(json_string) + response["url"] = server_url + print(f"Extracted properties from api response: {response}") + + return response diff --git a/llm-server/routes/workflow/generate_openapi_payload.py b/llm-server/routes/workflow/generate_openapi_payload.py index 5d3e21eb5..54fac5fea 100644 --- a/llm-server/routes/workflow/generate_openapi_payload.py +++ b/llm-server/routes/workflow/generate_openapi_payload.py @@ -44,23 +44,14 @@ def get_api_info_by_operation_id(data: Any, target_operation_id: str) -> ApiInfo api_info.endpoint = path api_info.method = method.upper() - # Extract path parameters and their schemas - path_params = {} - for parameter in details.get("parameters", []): - if parameter["in"] == "path": - param_name = parameter["name"] - param_schema = parameter.get("schema", {}) - path_params[param_name] = param_schema - api_info.path_params = path_params - - # Extract query parameters and their schemas - query_params = {} - for parameter in details.get("parameters", []): - if parameter["in"] == "query": - param_name = parameter["name"] - param_schema = parameter.get("schema", {}) - query_params[param_name] = param_schema - api_info.query_params = query_params + all_params = details.get("parameters", []) + api_info.path_params = { + "properties": [obj for obj in all_params if obj["in"] == "path"] + } + + api_info.query_params = { + "properties": [obj for obj in all_params if obj["in"] == "query"] + } # Extract request body schema if "requestBody" in details: @@ -101,14 +92,14 @@ def generate_openapi_payload( api_info.path_params = ( {} - if not api_info.path_params + if not api_info.path_params["properties"] else gen_params_from_schema( json.dumps(api_info.path_params), text, prev_api_response ) ) api_info.query_params = ( {} - if not api_info.query_params + if not api_info.query_params["properties"] else gen_params_from_schema( json.dumps(api_info.query_params), text, prev_api_response ) diff --git a/llm-server/routes/workflow/workflow_controller.py b/llm-server/routes/workflow/workflow_controller.py index bb15fd376..8acb964a4 100644 --- a/llm-server/routes/workflow/workflow_controller.py +++ b/llm-server/routes/workflow/workflow_controller.py @@ -4,6 +4,7 @@ from bson import ObjectId, json_util from copilot_exceptions.handle_exceptions_and_errors import handle_exceptions_and_errors +from utils.vector_db.add_workflow import add_workflow_data_to_qdrant from flask import Blueprint, request, jsonify from langchain.docstore.document import Document from opencopilot_types.workflow_type import WorkflowDataType @@ -158,21 +159,3 @@ def run_workflow_controller() -> Any: ) return result - -def add_workflow_data_to_qdrant( - workflow_id: str, workflow_data: Any, swagger_url: str -) -> None: - for flow in workflow_data["flows"]: - docs = [ - Document( - page_content=flow["description"], - metadata={ - "workflow_id": str(workflow_id), - "workflow_name": workflow_data.get("name"), - "swagger_id": workflow_data.get("swagger_id"), - "swagger_url": swagger_url, - }, - ) - ] - embeddings = get_embeddings() - init_vector_store(docs, embeddings, StoreOptions(swagger_url)) diff --git a/llm-server/routes/workflow/workflow_service.py b/llm-server/routes/workflow/workflow_service.py index 986466b0e..4b301d184 100644 --- a/llm-server/routes/workflow/workflow_service.py +++ b/llm-server/routes/workflow/workflow_service.py @@ -9,6 +9,7 @@ from utils.make_api_call import make_api_request from utils.vector_db.get_vector_store import get_vector_store from utils.vector_db.store_options import StoreOptions +import traceback db_instance = Database() mongo = db_instance.get_db() @@ -16,7 +17,7 @@ import os import logging -SCORE_THRESOLD = float(os.getenv("SCORE_THRESOLD", 0.88)) +SCORE_THRESHOLD = float(os.getenv("SCORE_THRESOLD", 0.88)) def get_valid_url( @@ -53,7 +54,7 @@ def run_workflow(data: WorkflowData, swagger_json: Any) -> Any: try: vector_store = get_vector_store(StoreOptions(namespace=data.swagger_url)) (document, score) = vector_store.similarity_search_with_relevance_scores( - text, score_threshold=SCORE_THRESOLD + text, score_threshold=SCORE_THRESHOLD )[0] logging.info( @@ -94,14 +95,28 @@ def run_openapi_operations( record_info = {"Workflow Name": record.get("name")} for flow in record.get("flows", []): prev_api_response = "" + for step in flow.get("steps"): - operation_id = step.get("open_api_operation_id") - api_payload = generate_openapi_payload( - swagger_json, text, operation_id, prev_api_response - ) - - api_response = make_api_request(headers=headers, **api_payload.__dict__) - record_info[operation_id] = json.loads(api_response.text) - prev_api_response = api_response.text + try: + operation_id = step.get("open_api_operation_id") + api_payload = generate_openapi_payload( + swagger_json, text, operation_id, prev_api_response + ) + + api_response = make_api_request(headers=headers, **api_payload.__dict__) + record_info[operation_id] = json.loads(api_response.text) + prev_api_response = api_response.text + + except Exception as e: + logging.error("Error making API call", exc_info=True) + + error_info = { + "operation_id": operation_id, + "error": str(e), + "traceback": traceback.format_exc(), + } + + record_info[operation_id] = error_info + prev_api_response = "" - return json.dumps(record_info) + return json.dumps(record_info) \ No newline at end of file diff --git a/llm-server/utils/vector_db/add_workflow.py b/llm-server/utils/vector_db/add_workflow.py new file mode 100644 index 000000000..56030a211 --- /dev/null +++ b/llm-server/utils/vector_db/add_workflow.py @@ -0,0 +1,24 @@ +from typing import Any +from utils.vector_db.store_options import StoreOptions +from langchain.docstore.document import Document +from utils.get_embeddings import get_embeddings +from utils.vector_db.init_vector_store import init_vector_store + + +def add_workflow_data_to_qdrant( + workflow_id: str, workflow_data: Any, swagger_url: str +) -> None: + for flow in workflow_data["flows"]: + docs = [ + Document( + page_content=flow["description"], + metadata={ + "workflow_id": str(workflow_id), + "workflow_name": workflow_data.get("name"), + "swagger_id": workflow_data.get("swagger_id"), + "swagger_url": swagger_url, + }, + ) + ] + embeddings = get_embeddings() + init_vector_store(docs, embeddings, StoreOptions(swagger_url)) From 99278b551875f22f4f16a691e276015071319cb4 Mon Sep 17 00:00:00 2001 From: codebane Date: Thu, 5 Oct 2023 22:37:14 +0300 Subject: [PATCH 2/7] refactored the code and included changes from closed prs --- llm-server/api_caller/base.py | 2 +- llm-server/routes/root_service.py | 81 ++++++--------- .../workflow/typings/run_workflow_input.py | 1 + llm-server/routes/workflow/utils/__init__.py | 8 ++ .../workflow/utils/check_workflow_in_store.py | 27 +++++ .../create_workflow_from_operation_ids.py | 34 +++++++ .../utils/detect_multiple_intents.py | 53 ++++++---- .../workflow/utils/fetch_swagger_text.py | 51 ++++++++++ .../workflow/utils/get_swagger_op_by_id.py | 16 +++ .../routes/workflow/utils/run_openapi_ops.py | 42 ++++++++ .../routes/workflow/utils/run_workflow.py | 17 ++++ .../routes/workflow/workflow_service.py | 99 +------------------ 12 files changed, 261 insertions(+), 170 deletions(-) create mode 100644 llm-server/routes/workflow/utils/__init__.py create mode 100644 llm-server/routes/workflow/utils/check_workflow_in_store.py create mode 100644 llm-server/routes/workflow/utils/create_workflow_from_operation_ids.py rename llm-server/{ => routes/workflow}/utils/detect_multiple_intents.py (61%) create mode 100644 llm-server/routes/workflow/utils/fetch_swagger_text.py create mode 100644 llm-server/routes/workflow/utils/get_swagger_op_by_id.py create mode 100644 llm-server/routes/workflow/utils/run_openapi_ops.py create mode 100644 llm-server/routes/workflow/utils/run_workflow.py diff --git a/llm-server/api_caller/base.py b/llm-server/api_caller/base.py index d5eeff32d..14ac3f7d6 100644 --- a/llm-server/api_caller/base.py +++ b/llm-server/api_caller/base.py @@ -5,7 +5,7 @@ def try_to_match_and_call_api_endpoint( - swagger_spec: OpenAPISpec, text: str, headers: Dict[str, str] + swagger_spec: OpenAPISpec, text: str, headers: Dict[str, str] ) -> str: openapi_call_chain = get_openapi_chain(swagger_spec, verbose=True, headers=headers) diff --git a/llm-server/routes/root_service.py b/llm-server/routes/root_service.py index b32e496f3..31713b78a 100644 --- a/llm-server/routes/root_service.py +++ b/llm-server/routes/root_service.py @@ -3,7 +3,6 @@ from typing import Dict, Any, cast import logging -import requests import traceback from dotenv import load_dotenv from langchain.chains.openai_functions import create_structured_output_chain @@ -13,16 +12,19 @@ from models.models import AiResponseFormat from prompts.base import api_base_prompt, non_api_base_prompt from routes.workflow.typings.run_workflow_input import WorkflowData -from routes.workflow.workflow_service import run_workflow -from utils.detect_multiple_intents import hasSingleIntent +from routes.workflow.utils import ( + run_workflow, + check_workflow_in_store, + fetch_swagger_text, + hasSingleIntent, + create_workflow_from_operation_ids, +) +from bson import ObjectId import os from dotenv import load_dotenv from typing import Dict, Any, cast from utils.db import Database -from utils.detect_multiple_intents import hasSingleIntent import json -import yaml -from yaml.parser import ParserError from api_caller.base import try_to_match_and_call_api_endpoint db_instance = Database() @@ -40,48 +42,6 @@ FAILED_TO_CALL_API_ENDPOINT = "Failed to call or map API endpoint" -def fetch_swagger_text(swagger_url: str) -> str: - if swagger_url.startswith("https://"): - response = requests.get(swagger_url) - if response.status_code == 200: - try: - # Try parsing the content as JSON - json_content = json.loads(response.text) - return json.dumps(json_content, indent=2) - except json.JSONDecodeError: - try: - # Try parsing the content as YAML - yaml_content = yaml.safe_load(response.text) - if isinstance(yaml_content, dict): - return json.dumps(yaml_content, indent=2) - else: - raise Exception("Invalid YAML content") - except ParserError: - raise Exception("Failed to parse content as JSON or YAML") - - raise Exception("Failed to fetch Swagger content") - - try: - with open(shared_folder + swagger_url, "r") as file: - content = file.read() - try: - # Try parsing the content as JSON - json_content = json.loads(content) - return json.dumps(json_content, indent=2) - except json.JSONDecodeError: - try: - # Try parsing the content as YAML - yaml_content = yaml.safe_load(content) - if isinstance(yaml_content, dict): - return json.dumps(yaml_content, indent=2) - else: - raise Exception("Invalid YAML content") - except ParserError: - raise Exception("Failed to parse content as JSON or YAML") - except FileNotFoundError: - raise Exception("File not found") - - def handle_request(data: Dict[str, Any]) -> Any: text: str = cast(str, data.get("text")) swagger_url = cast(str, data.get("swagger_url", "")) @@ -99,7 +59,7 @@ def handle_request(data: Dict[str, Any]) -> Any: if not locals()[required_field]: raise Exception(error_msg) - swagger_doc = mongo.swagger_files.find_one( + swagger_doc: Dict[str, Any] = mongo.swagger_files.find_one( {"meta.swagger_url": swagger_url}, {"meta": 0, "_id": 0} ) or json.loads(fetch_swagger_text(swagger_url)) @@ -114,9 +74,26 @@ def handle_request(data: Dict[str, Any]) -> Any: "[OpenCopilot] Apparently, the user request require calling more than single API endpoint " "to get the job done" ) + + # check workflow in mongodb, if present use that, else ask planner to create a workflow based on summaries + # then call run_workflow on that + (document, score) = check_workflow_in_store(text, swagger_url) + + _workflow = None + if document: + _workflow = mongo.workflows.find_one( + {"_id": ObjectId(document.metadata["workflow_id"])} + ) + else: + _workflow = create_workflow_from_operation_ids( + bot_response.ids, SWAGGER_SPEC=swagger_doc + ) return run_workflow( - WorkflowData(text, headers, server_base_url, swagger_url), swagger_doc + _workflow, + swagger_doc, + WorkflowData(text, headers, server_base_url, swagger_url), ) + elif len(bot_response.ids) == 0: logging.info("[OpenCopilot] The user request doesnot require an api call") return {"response": bot_response.bot_message} @@ -143,9 +120,7 @@ def handle_request(data: Dict[str, Any]) -> Any: ) json_output = try_to_match_and_call_api_endpoint(swagger_spec, text, headers) - formatted_response = json.dumps( - json_output, indent=4 - ) # Indent the JSON with 4 spaces + formatted_response = json.dumps(json_output, indent=4) logging.info( "[OpenCopilot] We were able to match and call the API endpoint, the response was: {}".format( formatted_response diff --git a/llm-server/routes/workflow/typings/run_workflow_input.py b/llm-server/routes/workflow/typings/run_workflow_input.py index 1cc0d0e1c..88877a373 100644 --- a/llm-server/routes/workflow/typings/run_workflow_input.py +++ b/llm-server/routes/workflow/typings/run_workflow_input.py @@ -1,6 +1,7 @@ from typing import Dict, Optional +# This is the api payload and doesnot represent workflow schema, use WorkflowDataType from opencopilot types for that class WorkflowData: def __init__( self, diff --git a/llm-server/routes/workflow/utils/__init__.py b/llm-server/routes/workflow/utils/__init__.py new file mode 100644 index 000000000..7f5dfb1a7 --- /dev/null +++ b/llm-server/routes/workflow/utils/__init__.py @@ -0,0 +1,8 @@ +from .run_workflow import * +from .check_workflow_in_store import * +from .create_workflow_from_operation_ids import * +from .detect_multiple_intents import * +from .fetch_swagger_text import * +from .get_swagger_op_by_id import * +from .run_openapi_ops import * +from .run_workflow import * \ No newline at end of file diff --git a/llm-server/routes/workflow/utils/check_workflow_in_store.py b/llm-server/routes/workflow/utils/check_workflow_in_store.py new file mode 100644 index 000000000..eecb57cc8 --- /dev/null +++ b/llm-server/routes/workflow/utils/check_workflow_in_store.py @@ -0,0 +1,27 @@ +from typing import Any, Dict, Optional + +from langchain.vectorstores.base import VectorStore +from langchain.docstore.document import Document +from typing import Tuple +from utils.vector_db.get_vector_store import get_vector_store +from utils.vector_db.store_options import StoreOptions +import logging, os + + +def check_workflow_in_store( + text: str, namespace: str +) -> Tuple[Optional[Document], Optional[float]]: + score_threshold = os.getenv("SCORE_THRESHOLD", 0.95) + vector_store = get_vector_store(StoreOptions(namespace)) + + try: + result = vector_store.similarity_search_with_relevance_scores( + text, score_threshold=score_threshold + )[0] + + document, score = result + return document, score + + except Exception as e: + logging.info(f"[Error] {e}") + return None, None diff --git a/llm-server/routes/workflow/utils/create_workflow_from_operation_ids.py b/llm-server/routes/workflow/utils/create_workflow_from_operation_ids.py new file mode 100644 index 000000000..bb4c8099f --- /dev/null +++ b/llm-server/routes/workflow/utils/create_workflow_from_operation_ids.py @@ -0,0 +1,34 @@ +from typing import Any, List, Dict +from routes.workflow.utils.get_swagger_op_by_id import get_operation_by_id +from opencopilot_types.workflow_type import WorkflowDataType + + +def create_workflow_from_operation_ids( + op_ids: List[str], SWAGGER_SPEC: Dict[str, Any] +) -> Any: + flows = [] + + for op_id in op_ids: + operation = get_operation_by_id(SWAGGER_SPEC, op_id) + step = { + "stepId": str(op_ids.index(op_id)), + "operation": "call", + "open_api_operation_id": op_id, + } + flow = { + "name": operation["name"], + "description": operation["description"], + "requires_confirmation": False, + "steps": [step], + "on_success": [{"handler": "plotOutcomeJsFunction"}], + "on_failure": [{"handler": "plotOutcomeJsFunction"}], + } + flows.append(flow) + + workflow: WorkflowDataType = { + "opencopilot": "0.1", + "info": {"title": "", "version": "1.0.0"}, + "flows": flows, + } + + return workflow diff --git a/llm-server/utils/detect_multiple_intents.py b/llm-server/routes/workflow/utils/detect_multiple_intents.py similarity index 61% rename from llm-server/utils/detect_multiple_intents.py rename to llm-server/routes/workflow/utils/detect_multiple_intents.py index 8a31a16d7..5a971a1cd 100644 --- a/llm-server/utils/detect_multiple_intents.py +++ b/llm-server/routes/workflow/utils/detect_multiple_intents.py @@ -11,7 +11,7 @@ import os from dotenv import load_dotenv import logging - +from prance import ResolvingParser logging.basicConfig(level=logging.DEBUG) load_dotenv() @@ -30,29 +30,42 @@ def from_dict(cls, data: Dict[str, Union[str, List[str]]]) -> "BotMessage": return cls(cast(List[str], data["ids"]), cast(str, data["bot_message"])) -def getSummaries(swagger_doc: Any): - """Get API endpoint summaries from an OpenAPI spec.""" - - summaries: List[str] = [] - - # Get the paths and iterate over them - paths: Optional[Dict[str, Any]] = swagger_doc.get("paths") - if not paths: - raise ValueError("OpenAPI spec missing 'paths'") - +def get_summaries(_swagger_doc: str) -> str: + swagger_doc = ResolvingParser(spec_string=_swagger_doc) + servers = ", ".join( + [s["url"] for s in swagger_doc.specification.get("servers", [])] + ) + summaries_str = "servers:" + servers + "\n" + paths = swagger_doc.specification.get("paths") for path in paths: - operation = paths[path] - for field in operation: - if "summary" in operation[field]: - summaries.append( - f"""{operation[field]["operationId"]} - {operation[field]["description"]}""" - ) - - return summaries + operations = paths[path] + for method in operations: + operation = operations[method] + try: + summary = f"- {operation['operationId']} - {operation['summary']}\n" + if "requestBody" in operation: + content_types = operation["requestBody"]["content"] + if "application/json" in content_types: + schema = content_types["application/json"]["schema"] + if "properties" in schema: + params = schema["properties"].keys() + elif "items" in schema: + params = schema["items"]["properties"].keys() + elif "application/octet-stream" in content_types: + params = ["binary data"] + summary += f" - Body Parameters: {', '.join(params)}\n" + summary += f" - Method: {method}\n" + if "parameters" in operation: + params = [p["name"] for p in operation["parameters"]] + summary += f" - Parameters: {', '.join(params)}\n" + summaries_str += summary + "\n" + except: + pass + return summaries_str def hasSingleIntent(swagger_doc: Any, user_requirement: str) -> BotMessage: - summaries = getSummaries(swagger_doc) + summaries = get_summaries(swagger_doc) chat = ChatOpenAI( openai_api_key=os.getenv("OPENAI_API_KEY"), diff --git a/llm-server/routes/workflow/utils/fetch_swagger_text.py b/llm-server/routes/workflow/utils/fetch_swagger_text.py new file mode 100644 index 000000000..531ba503b --- /dev/null +++ b/llm-server/routes/workflow/utils/fetch_swagger_text.py @@ -0,0 +1,51 @@ +import json, requests, yaml, os +from typing import Dict, Any, cast +from dotenv import load_dotenv + +load_dotenv() +shared_folder = os.getenv("SHARED_FOLDER", "/app/shared_data/") + +import json +import yaml +from yaml.parser import ParserError + +def fetch_swagger_text(swagger_url: str) -> str: + if swagger_url.startswith("https://"): + response = requests.get(swagger_url) + if response.status_code == 200: + try: + # Try parsing the content as JSON + json_content = json.loads(response.text) + return json.dumps(json_content, indent=2) + except json.JSONDecodeError: + try: + # Try parsing the content as YAML + yaml_content = yaml.safe_load(response.text) + if isinstance(yaml_content, dict): + return json.dumps(yaml_content, indent=2) + else: + raise Exception("Invalid YAML content") + except ParserError: + raise Exception("Failed to parse content as JSON or YAML") + + raise Exception("Failed to fetch Swagger content") + + try: + with open(shared_folder + swagger_url, "r") as file: + content = file.read() + try: + # Try parsing the content as JSON + json_content = json.loads(content) + return json.dumps(json_content, indent=2) + except json.JSONDecodeError: + try: + # Try parsing the content as YAML + yaml_content = yaml.safe_load(content) + if isinstance(yaml_content, dict): + return json.dumps(yaml_content, indent=2) + else: + raise Exception("Invalid YAML content") + except ParserError: + raise Exception("Failed to parse content as JSON or YAML") + except FileNotFoundError: + raise Exception("File not found") diff --git a/llm-server/routes/workflow/utils/get_swagger_op_by_id.py b/llm-server/routes/workflow/utils/get_swagger_op_by_id.py new file mode 100644 index 000000000..7270764e2 --- /dev/null +++ b/llm-server/routes/workflow/utils/get_swagger_op_by_id.py @@ -0,0 +1,16 @@ +from typing import Dict, Any + + +def get_operation_by_id(swagger_spec: Dict[str, Any], op_id_key: str) -> Dict[str, Any]: + operation_lookup = {} + + for path in swagger_spec["paths"]: + for method in swagger_spec["paths"][path]: + operation = swagger_spec["paths"][path][method] + operation_id = operation["operationId"] + operation_lookup[operation_id] = { + "name": operation.get("name"), + "description": operation.get("description"), + } + + return operation_lookup[op_id_key] diff --git a/llm-server/routes/workflow/utils/run_openapi_ops.py b/llm-server/routes/workflow/utils/run_openapi_ops.py new file mode 100644 index 000000000..b7e63d6d0 --- /dev/null +++ b/llm-server/routes/workflow/utils/run_openapi_ops.py @@ -0,0 +1,42 @@ +import json +from routes.workflow.generate_openapi_payload import generate_openapi_payload +from utils.make_api_call import make_api_request +import traceback +import logging +from typing import Any + +def run_openapi_operations( + record: Any, + swagger_json: str, + text: str, + headers: Any, + server_base_url: str, +) -> str: + record_info = {"Workflow Name": record.get("name")} + for flow in record.get("flows", []): + prev_api_response = "" + + for step in flow.get("steps"): + try: + operation_id = step.get("open_api_operation_id") + api_payload = generate_openapi_payload( + swagger_json, text, operation_id, prev_api_response + ) + + api_response = make_api_request(headers=headers, **api_payload.__dict__) + record_info[operation_id] = json.loads(api_response.text) + prev_api_response = api_response.text + + except Exception as e: + logging.error("Error making API call", exc_info=True) + + error_info = { + "operation_id": operation_id, + "error": str(e), + "traceback": traceback.format_exc(), + } + + record_info[operation_id] = error_info + + prev_api_response = "" + return json.dumps(record_info) \ No newline at end of file diff --git a/llm-server/routes/workflow/utils/run_workflow.py b/llm-server/routes/workflow/utils/run_workflow.py new file mode 100644 index 000000000..164c91e54 --- /dev/null +++ b/llm-server/routes/workflow/utils/run_workflow.py @@ -0,0 +1,17 @@ +from typing import Any, Dict +from routes.workflow.typings.run_workflow_input import WorkflowData +from routes.workflow.utils.run_openapi_ops import run_openapi_operations +from opencopilot_types.workflow_type import WorkflowDataType + + +def run_workflow( + workflow_doc: WorkflowDataType, swagger_json: Any, data: WorkflowData +) -> Dict[str, Any]: + headers = data.headers or {} + server_base_url = data.server_base_url + + result = run_openapi_operations( + workflow_doc, swagger_json, data.text, headers, server_base_url + ) + + return {"response": result} diff --git a/llm-server/routes/workflow/workflow_service.py b/llm-server/routes/workflow/workflow_service.py index 4b301d184..2ba75b140 100644 --- a/llm-server/routes/workflow/workflow_service.py +++ b/llm-server/routes/workflow/workflow_service.py @@ -1,21 +1,12 @@ -import json from typing import Any, Dict, Optional, Union - -from bson import ObjectId -from routes.workflow.generate_openapi_payload import generate_openapi_payload -from routes.workflow.hierarchical_planner import create_and_run_openapi_agent -from routes.workflow.typings.run_workflow_input import WorkflowData from utils.db import Database -from utils.make_api_call import make_api_request -from utils.vector_db.get_vector_store import get_vector_store -from utils.vector_db.store_options import StoreOptions -import traceback db_instance = Database() mongo = db_instance.get_db() +from dotenv import load_dotenv +load_dotenv() import os -import logging SCORE_THRESHOLD = float(os.getenv("SCORE_THRESOLD", 0.88)) @@ -35,88 +26,4 @@ def get_valid_url( else: raise ValueError("Invalid server_base_url") else: - raise ValueError("Missing path parameter") - - -def run_workflow(data: WorkflowData, swagger_json: Any) -> Any: - logging.info( - "[OpenCopilot] Trying to map the user request with a flow (since the request need multiple API calls)" - ) - text = data.text - headers = data.headers or {} - # This will come from the request payload later on when implementing multi-tenancy - namespace = "workflows" - server_base_url = data.server_base_url - - if not text: - return json.dumps({"error": "text is required"}), 400 - - try: - vector_store = get_vector_store(StoreOptions(namespace=data.swagger_url)) - (document, score) = vector_store.similarity_search_with_relevance_scores( - text, score_threshold=SCORE_THRESHOLD - )[0] - - logging.info( - f"[OpenCopilot] Record '{document}' is highly similar with a similarity score of {score} which means " - f"we can run this flow" - ) - first_document_id = ( - ObjectId(document.metadata["workflow_id"]) if document else None - ) - record = mongo.workflows.find_one({"_id": first_document_id}) - - result = run_openapi_operations( - record, swagger_json, text, headers, server_base_url - ) - return {"response": result} - - except Exception as e: - # Log the error, but continue with the rest of the code - logging.info(f"[OpenCopilot] Error fetching data from namespace '{namespace}': {str(e)}") - - logging.info(f"[OpenCopilot] Could not map the user request to a flow, last attempt is to run the hierarchical " - f"planning AI") - - # Call openapi spec even if an error occurred with Qdrant - result = create_and_run_openapi_agent(swagger_json, text, headers) - - logging.info("[OpenCopilot] Planner out come {}".format(json.dumps({"response": result}))) - return {"response": result} - - -def run_openapi_operations( - record: Any, - swagger_json: str, - text: str, - headers: Any, - server_base_url: str, -) -> str: - record_info = {"Workflow Name": record.get("name")} - for flow in record.get("flows", []): - prev_api_response = "" - - for step in flow.get("steps"): - try: - operation_id = step.get("open_api_operation_id") - api_payload = generate_openapi_payload( - swagger_json, text, operation_id, prev_api_response - ) - - api_response = make_api_request(headers=headers, **api_payload.__dict__) - record_info[operation_id] = json.loads(api_response.text) - prev_api_response = api_response.text - - except Exception as e: - logging.error("Error making API call", exc_info=True) - - error_info = { - "operation_id": operation_id, - "error": str(e), - "traceback": traceback.format_exc(), - } - - record_info[operation_id] = error_info - - prev_api_response = "" - return json.dumps(record_info) \ No newline at end of file + raise ValueError("Missing path parameter") \ No newline at end of file From 51b60347d69b56994ba55ca1890e50dff4a03228 Mon Sep 17 00:00:00 2001 From: codebane Date: Thu, 5 Oct 2023 23:58:51 +0300 Subject: [PATCH 3/7] Converting completion apis to chat apis for large context window --- .../workflow/extractors/extract_body.py | 68 +++++++------------ .../workflow/extractors/extract_param.py | 55 +++++++-------- 2 files changed, 51 insertions(+), 72 deletions(-) diff --git a/llm-server/routes/workflow/extractors/extract_body.py b/llm-server/routes/workflow/extractors/extract_body.py index 7e2c2efe5..ba5a3618a 100644 --- a/llm-server/routes/workflow/extractors/extract_body.py +++ b/llm-server/routes/workflow/extractors/extract_body.py @@ -1,11 +1,12 @@ import os -from langchain.prompts import PromptTemplate -from langchain.chains import LLMChain +from langchain.schema import AIMessage, HumanMessage, SystemMessage +from langchain.chat_models import ChatOpenAI from utils.get_llm import get_llm from typing import Any from routes.workflow.extractors.extract_json import extract_json_payload from custom_types.t_json import JsonData +import logging openai_api_key = os.getenv("OPENAI_API_KEY") llm = get_llm() @@ -14,47 +15,30 @@ def gen_body_from_schema( body_schema: str, text: str, prev_api_response: str, example: str ) -> Any: - _DEFAULT_TEMPLATE = """To enable a substantially intelligent language model to execute a series of APIs sequentially, the following essential details are necessary to gather information needed for the next API call: - 1. Initial input when starting the flow: `{text}` - 2. Previous API responses: `{prev_api_response}` - 3. A JSON response schema that defines the expected format: `{body_schema}` - - Try to adhere to this sample api payload as much as possible: ```{example}``` - The JSON payload, enclosed within triple backticks on both sides, strictly conforming to the specified "type/format" as outlined in the schema is as follows: - """ - - PROMPT = PromptTemplate( - input_variables=[ - "text", - "body_schema", - "prev_api_response", - "example", - ], - template=_DEFAULT_TEMPLATE, + chat = ChatOpenAI( + openai_api_key=os.getenv("OPENAI_API_KEY"), + model="gpt-3.5-turbo-16k", + temperature=0, ) - PROMPT.format( - prev_api_response=prev_api_response, - body_schema=body_schema, - text=text, - example=example, + messages = [ + SystemMessage( + content="You are an intelligent machine learning model that can produce REST API's body in json format, given the json schema, dummy json payload, user input, data from previous api calls." + ), + HumanMessage(content="Json Schema: {}".format(body_schema)), + HumanMessage(content="Dummy json payload: {}".format(example)), + HumanMessage(content="User input: {}".format(text)), + HumanMessage(content="prev api responses: {}".format(prev_api_response)), + ] + result = chat(messages) + + logging.info("[OpenCopilot] LLM Body Response: {}".format(result.content)) + + d: Any = extract_json_payload(result.content) + logging.info( + "[OpenCopilot] Parsed the json payload: {}, context: {}".format( + d, "gen_body_from_schema" + ) ) - chain = LLMChain( - llm=llm, - prompt=PROMPT, - # memory=memory, - verbose=True, - ) - json_string = chain.run( - { - "text": text, - "body_schema": body_schema, - "prev_api_response": prev_api_response, - "example": example, - } - ) - - response = extract_json_payload(json_string) - - return response + return d diff --git a/llm-server/routes/workflow/extractors/extract_param.py b/llm-server/routes/workflow/extractors/extract_param.py index d168bf7e7..08e80c4bd 100644 --- a/llm-server/routes/workflow/extractors/extract_param.py +++ b/llm-server/routes/workflow/extractors/extract_param.py @@ -1,10 +1,11 @@ import os -from langchain.prompts import PromptTemplate -from langchain.chains import LLMChain +from langchain.chat_models import ChatOpenAI from routes.workflow.extractors.extract_json import extract_json_payload from utils.get_llm import get_llm from custom_types.t_json import JsonData -from typing import Optional +from typing import Optional, Any +import logging +from langchain.schema import HumanMessage, SystemMessage openai_api_key = os.getenv("OPENAI_API_KEY") llm = get_llm() @@ -13,35 +14,29 @@ def gen_params_from_schema( param_schema: str, text: str, prev_resp: str ) -> Optional[JsonData]: - - _DEFAULT_TEMPLATE = """We have the following information required for executing the next API call.\n - The initial input at the onset of the process: `{text}`\n - The responses obtained from previous API calls: ```{prev_resp}``` - Swagger Schema defining parameters: ```{param_schema}``` - Output musts be a json object representing the query params and nothing else. Also query params should only have keys defined in the swagger schema ignore anything else, moreover if user instruction doesnot have information about a query parameter ignore that as well. - - Query params: """ - - PROMPT = PromptTemplate( - input_variables=["prev_resp", "text", "param_schema"], - template=_DEFAULT_TEMPLATE, + chat = ChatOpenAI( + openai_api_key=os.getenv("OPENAI_API_KEY"), + model="gpt-3.5-turbo-16k", + temperature=0, ) - PROMPT.format( - prev_resp=prev_resp, - text=text, - param_schema=param_schema, - ) + messages = [ + SystemMessage( + content="You are an intelligent machine learning model that can produce REST API's params / query params in json format, given the json schema, user input, data from previous api calls." + ), + HumanMessage(content="Json Schema: {}".format(param_schema)), + HumanMessage(content="User input: {}".format(text)), + HumanMessage(content="prev api responses: {}".format(prev_resp)), + ] + result = chat(messages) + + logging.info("[OpenCopilot] LLM Body Response: {}".format(result.content)) - chain = LLMChain(llm=llm, prompt=PROMPT, verbose=True) - json_string = chain.run( - { - "param_schema": param_schema, - "text": text, - "prev_resp": prev_resp, - } + d: Optional[JsonData] = extract_json_payload(result.content) + logging.info( + "[OpenCopilot] Parsed the json payload: {}, context: {}".format( + d, "gen_body_from_schema" + ) ) - response = extract_json_payload(json_string) - print(f"Query params: {response}") - return response + return d From e87ccdf26bda8c30cc9568d1ccde9dc1d212f59f Mon Sep 17 00:00:00 2001 From: codebane Date: Fri, 6 Oct 2023 00:07:01 +0300 Subject: [PATCH 4/7] fixing imports --- llm-server/routes/workflow/workflow_controller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/llm-server/routes/workflow/workflow_controller.py b/llm-server/routes/workflow/workflow_controller.py index 8acb964a4..0c78f07ee 100644 --- a/llm-server/routes/workflow/workflow_controller.py +++ b/llm-server/routes/workflow/workflow_controller.py @@ -10,7 +10,7 @@ from opencopilot_types.workflow_type import WorkflowDataType from routes.workflow.typings.run_workflow_input import WorkflowData from routes.workflow.validate_json import validate_json -from routes.workflow.workflow_service import run_workflow +from routes.workflow.utils import run_workflow from utils.db import Database from utils.get_embeddings import get_embeddings from utils.vector_db.get_vector_store import get_vector_store @@ -158,4 +158,3 @@ def run_workflow_controller() -> Any: swagger_json, ) return result - From 5a9982fae2fa541bfce3a8813e6c94140d73db46 Mon Sep 17 00:00:00 2001 From: codebane Date: Fri, 6 Oct 2023 00:21:02 +0300 Subject: [PATCH 5/7] adding required prompts for chat apis to work correctly --- llm-server/routes/workflow/extractors/extract_body.py | 3 +++ llm-server/routes/workflow/extractors/extract_param.py | 3 +++ llm-server/routes/workflow/generate_openapi_payload.py | 2 ++ 3 files changed, 8 insertions(+) diff --git a/llm-server/routes/workflow/extractors/extract_body.py b/llm-server/routes/workflow/extractors/extract_body.py index ba5a3618a..b47d240ae 100644 --- a/llm-server/routes/workflow/extractors/extract_body.py +++ b/llm-server/routes/workflow/extractors/extract_body.py @@ -29,6 +29,9 @@ def gen_body_from_schema( HumanMessage(content="Dummy json payload: {}".format(example)), HumanMessage(content="User input: {}".format(text)), HumanMessage(content="prev api responses: {}".format(prev_api_response)), + HumanMessage( + content="Given the provided information, generate the appropriate JSON payload to use as body for the API request" + ), ] result = chat(messages) diff --git a/llm-server/routes/workflow/extractors/extract_param.py b/llm-server/routes/workflow/extractors/extract_param.py index 08e80c4bd..fb65ce8c5 100644 --- a/llm-server/routes/workflow/extractors/extract_param.py +++ b/llm-server/routes/workflow/extractors/extract_param.py @@ -27,6 +27,9 @@ def gen_params_from_schema( HumanMessage(content="Json Schema: {}".format(param_schema)), HumanMessage(content="User input: {}".format(text)), HumanMessage(content="prev api responses: {}".format(prev_resp)), + HumanMessage( + content="Given the provided information, generate the appropriate JSON payload to use as parameters for the API request" + ), ] result = chat(messages) diff --git a/llm-server/routes/workflow/generate_openapi_payload.py b/llm-server/routes/workflow/generate_openapi_payload.py index 54fac5fea..b9de60d08 100644 --- a/llm-server/routes/workflow/generate_openapi_payload.py +++ b/llm-server/routes/workflow/generate_openapi_payload.py @@ -110,6 +110,8 @@ def generate_openapi_payload( api_info.body_schema = gen_body_from_schema( json.dumps(api_info.body_schema), text, prev_api_response, example ) + # when you come back, clear the trello board and + # extract api info and set it up for next call else: api_info.body_schema = {} From 45fffd4987c4c09adf6f13b9e1f6f887b4e6405b Mon Sep 17 00:00:00 2001 From: codebane Date: Fri, 6 Oct 2023 02:16:17 +0300 Subject: [PATCH 6/7] Added a transformer function to extract important information between various api calls --- .../extractors/transform_api_response.py | 60 +++++++++++-------- .../workflow/generate_openapi_payload.py | 8 +++ .../routes/workflow/utils/run_openapi_ops.py | 13 ++-- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/llm-server/routes/workflow/extractors/transform_api_response.py b/llm-server/routes/workflow/extractors/transform_api_response.py index 82864f0bb..3fcc05eea 100644 --- a/llm-server/routes/workflow/extractors/transform_api_response.py +++ b/llm-server/routes/workflow/extractors/transform_api_response.py @@ -1,38 +1,48 @@ -import os -from langchain.prompts import PromptTemplate -from langchain.chains import LLMChain -from routes.workflow.extractors.extract_json import extract_json_payload -from utils.get_llm import get_llm +import os, logging +from langchain.chat_models import ChatOpenAI from custom_types.t_json import JsonData from typing import Optional +from dotenv import load_dotenv +from langchain.schema import HumanMessage, SystemMessage +from typing import Any +from routes.workflow.extractors.extract_json import extract_json_payload + +load_dotenv() openai_api_key = os.getenv("OPENAI_API_KEY") -llm = get_llm() def transform_api_response_from_schema( server_url: str, api_response: str ) -> Optional[JsonData]: - _DEFAULT_TEMPLATE = """Given the following response from an api call```{api_response}``` extract the parts of information necessary for making furthur api calls to a backend server. Just return a json payload wrapped between three back ticks. use descriptive keys when returning the json""" - - PROMPT = PromptTemplate( - input_variables=["prev_resp", "text", "param_schema"], - template=_DEFAULT_TEMPLATE, + chat = ChatOpenAI( + openai_api_key=os.getenv("OPENAI_API_KEY"), + model="gpt-3.5-turbo-16k", + temperature=0, ) - PROMPT.format( - api_response=api_response, + messages = [ + SystemMessage( + content="You are an intelligent AI assistant that can identify important fields from a REST API response." + ), + HumanMessage( + content="Here is the response from a REST API call: {} for endpoint: {}".format( + api_response, server_url + ) + ), + HumanMessage( + content="Please examine the given API response and return only the fields that are important when making API calls. Ignore any unimportant fields. Structure your response as a JSON object with self-descriptive keys mapped to the corresponding values from the API response." + ), + ] + + result = chat(messages) + logging.info("[OpenCopilot] LLM Body Response: {}".format(result.content)) + + d = extract_json_payload(result.content) + logging.info( + "[OpenCopilot] Parsed the json payload: {}, context: {}".format( + d, "gen_body_from_schema" + ) ) - chain = LLMChain(llm=llm, prompt=PROMPT, verbose=True) - json_string = chain.run( - { - "api_response": api_response, - } - ) - - response = extract_json_payload(json_string) - response["url"] = server_url - print(f"Extracted properties from api response: {response}") - - return response + return d diff --git a/llm-server/routes/workflow/generate_openapi_payload.py b/llm-server/routes/workflow/generate_openapi_payload.py index b9de60d08..d284cfd4a 100644 --- a/llm-server/routes/workflow/generate_openapi_payload.py +++ b/llm-server/routes/workflow/generate_openapi_payload.py @@ -1,6 +1,9 @@ import re import os import json +from routes.workflow.extractors.transform_api_response import ( + transform_api_response_from_schema, +) from utils.get_llm import get_llm from dotenv import load_dotenv from .extractors.example_generator import gen_ex_from_schema @@ -112,6 +115,11 @@ def generate_openapi_payload( ) # when you come back, clear the trello board and # extract api info and set it up for next call + transformed_response = transform_api_response_from_schema( + api_info.endpoint or "", api_info.body_schema + ) + + prev_api_response = prev_api_response + json.loads(transformed_response) else: api_info.body_schema = {} diff --git a/llm-server/routes/workflow/utils/run_openapi_ops.py b/llm-server/routes/workflow/utils/run_openapi_ops.py index b7e63d6d0..89bce4067 100644 --- a/llm-server/routes/workflow/utils/run_openapi_ops.py +++ b/llm-server/routes/workflow/utils/run_openapi_ops.py @@ -5,12 +5,13 @@ import logging from typing import Any + def run_openapi_operations( - record: Any, - swagger_json: str, - text: str, - headers: Any, - server_base_url: str, + record: Any, + swagger_json: str, + text: str, + headers: Any, + server_base_url: str, ) -> str: record_info = {"Workflow Name": record.get("name")} for flow in record.get("flows", []): @@ -39,4 +40,4 @@ def run_openapi_operations( record_info[operation_id] = error_info prev_api_response = "" - return json.dumps(record_info) \ No newline at end of file + return json.dumps(record_info) From caa37a87c443b2d2bbf3fd3efc626727318386dc Mon Sep 17 00:00:00 2001 From: codebane Date: Fri, 6 Oct 2023 05:13:30 +0300 Subject: [PATCH 7/7] Adding custom query planners --- llm-server/readme.md | 2 ++ .../workflow/extractors/extract_param.py | 2 +- .../extractors/transform_api_response.py | 23 ++++----------- .../workflow/generate_openapi_payload.py | 9 ------ .../routes/workflow/hierarchical_planner.py | 2 +- .../routes/workflow/utils/run_openapi_ops.py | 17 +++++++---- .../routes/workflow/utils/run_workflow.py | 28 ++++++++++++++++--- 7 files changed, 46 insertions(+), 37 deletions(-) diff --git a/llm-server/readme.md b/llm-server/readme.md index 51d348d92..6c3354474 100644 --- a/llm-server/readme.md +++ b/llm-server/readme.md @@ -63,6 +63,8 @@ To install Mypy, which is a static type checker for Python, follow these steps: MONGODB_URL=mongodb://localhost:27017/opencopilot QDRANT_URL=http://localhost:6333 STORE=QDRANT + QDRANT_API_KEY= # When using cloud hosted version + SCORE_THRESHOLD=0.95 # When using pre defined workflows, the confidence score at which the opencopilot should select your workflow. If the score falls below this, the planner will design it's own workflow ``` Ensure you replace the placeholders with your actual API keys and configuration settings. diff --git a/llm-server/routes/workflow/extractors/extract_param.py b/llm-server/routes/workflow/extractors/extract_param.py index fb65ce8c5..414fe17fb 100644 --- a/llm-server/routes/workflow/extractors/extract_param.py +++ b/llm-server/routes/workflow/extractors/extract_param.py @@ -28,7 +28,7 @@ def gen_params_from_schema( HumanMessage(content="User input: {}".format(text)), HumanMessage(content="prev api responses: {}".format(prev_resp)), HumanMessage( - content="Given the provided information, generate the appropriate JSON payload to use as parameters for the API request" + content="Based on the information provided, construct a valid parameter object to be used with python requests library. In cases where user input doesnot contain information for a query, DO NOT add that specific query parameter to the output. " ), ] result = chat(messages) diff --git a/llm-server/routes/workflow/extractors/transform_api_response.py b/llm-server/routes/workflow/extractors/transform_api_response.py index 3fcc05eea..9d8791550 100644 --- a/llm-server/routes/workflow/extractors/transform_api_response.py +++ b/llm-server/routes/workflow/extractors/transform_api_response.py @@ -1,7 +1,5 @@ import os, logging from langchain.chat_models import ChatOpenAI -from custom_types.t_json import JsonData -from typing import Optional from dotenv import load_dotenv from langchain.schema import HumanMessage, SystemMessage from typing import Any @@ -12,9 +10,7 @@ openai_api_key = os.getenv("OPENAI_API_KEY") -def transform_api_response_from_schema( - server_url: str, api_response: str -) -> Optional[JsonData]: +def transform_api_response_from_schema(server_url: str, api_response: str) -> str: chat = ChatOpenAI( openai_api_key=os.getenv("OPENAI_API_KEY"), model="gpt-3.5-turbo-16k", @@ -23,26 +19,19 @@ def transform_api_response_from_schema( messages = [ SystemMessage( - content="You are an intelligent AI assistant that can identify important fields from a REST API response." + content="You are a bot capable of comprehending API responses." ), HumanMessage( - content="Here is the response from a REST API call: {} for endpoint: {}".format( + content="Here is the response from current REST API: {} for endpoint: {}".format( api_response, server_url ) ), HumanMessage( - content="Please examine the given API response and return only the fields that are important when making API calls. Ignore any unimportant fields. Structure your response as a JSON object with self-descriptive keys mapped to the corresponding values from the API response." + content="Analyze the provided API responses and extract only the essential fields required for subsequent API interactions. Disregard any non-essential attributes such as CSS or color-related data. If there are generic fields like 'id,' provide them with more descriptive names in your response. Format your response as a JSON object with clear and meaningful keys that map to their respective values from the API response." ), ] result = chat(messages) - logging.info("[OpenCopilot] LLM Body Response: {}".format(result.content)) + logging.info("[OpenCopilot] Transformed Response: {}".format(result.content)) - d = extract_json_payload(result.content) - logging.info( - "[OpenCopilot] Parsed the json payload: {}, context: {}".format( - d, "gen_body_from_schema" - ) - ) - - return d + return result.content diff --git a/llm-server/routes/workflow/generate_openapi_payload.py b/llm-server/routes/workflow/generate_openapi_payload.py index d284cfd4a..91bc0f6f6 100644 --- a/llm-server/routes/workflow/generate_openapi_payload.py +++ b/llm-server/routes/workflow/generate_openapi_payload.py @@ -1,9 +1,6 @@ import re import os import json -from routes.workflow.extractors.transform_api_response import ( - transform_api_response_from_schema, -) from utils.get_llm import get_llm from dotenv import load_dotenv from .extractors.example_generator import gen_ex_from_schema @@ -113,13 +110,7 @@ def generate_openapi_payload( api_info.body_schema = gen_body_from_schema( json.dumps(api_info.body_schema), text, prev_api_response, example ) - # when you come back, clear the trello board and - # extract api info and set it up for next call - transformed_response = transform_api_response_from_schema( - api_info.endpoint or "", api_info.body_schema - ) - prev_api_response = prev_api_response + json.loads(transformed_response) else: api_info.body_schema = {} diff --git a/llm-server/routes/workflow/hierarchical_planner.py b/llm-server/routes/workflow/hierarchical_planner.py index 85cd23844..7da8a8b84 100644 --- a/llm-server/routes/workflow/hierarchical_planner.py +++ b/llm-server/routes/workflow/hierarchical_planner.py @@ -11,7 +11,7 @@ def create_and_run_openapi_agent( - swagger_json: Any, user_query: str, headers: Dict[str, str] = {} + swagger_json: Any, user_query: str, headers: Dict[str, str] = {} ) -> Any: # Load OpenAPI spec # raw_spec = json.loads(swagger_json) diff --git a/llm-server/routes/workflow/utils/run_openapi_ops.py b/llm-server/routes/workflow/utils/run_openapi_ops.py index 89bce4067..d3d3120d4 100644 --- a/llm-server/routes/workflow/utils/run_openapi_ops.py +++ b/llm-server/routes/workflow/utils/run_openapi_ops.py @@ -4,6 +4,9 @@ import traceback import logging from typing import Any +from routes.workflow.extractors.transform_api_response import ( + transform_api_response_from_schema, +) def run_openapi_operations( @@ -13,10 +16,9 @@ def run_openapi_operations( headers: Any, server_base_url: str, ) -> str: + prev_api_response = "" record_info = {"Workflow Name": record.get("name")} for flow in record.get("flows", []): - prev_api_response = "" - for step in flow.get("steps"): try: operation_id = step.get("open_api_operation_id") @@ -25,8 +27,13 @@ def run_openapi_operations( ) api_response = make_api_request(headers=headers, **api_payload.__dict__) + + transformed_response = transform_api_response_from_schema( + api_payload.endpoint or "", api_response.text + ) + + prev_api_response = prev_api_response + transformed_response record_info[operation_id] = json.loads(api_response.text) - prev_api_response = api_response.text except Exception as e: logging.error("Error making API call", exc_info=True) @@ -36,8 +43,8 @@ def run_openapi_operations( "error": str(e), "traceback": traceback.format_exc(), } - record_info[operation_id] = error_info - prev_api_response = "" + # At this point we will retry the operation with hierarchical planner + raise e return json.dumps(record_info) diff --git a/llm-server/routes/workflow/utils/run_workflow.py b/llm-server/routes/workflow/utils/run_workflow.py index 164c91e54..a3d6a301b 100644 --- a/llm-server/routes/workflow/utils/run_workflow.py +++ b/llm-server/routes/workflow/utils/run_workflow.py @@ -2,6 +2,8 @@ from routes.workflow.typings.run_workflow_input import WorkflowData from routes.workflow.utils.run_openapi_ops import run_openapi_operations from opencopilot_types.workflow_type import WorkflowDataType +from routes.workflow.hierarchical_planner import create_and_run_openapi_agent +import logging, json def run_workflow( @@ -10,8 +12,26 @@ def run_workflow( headers = data.headers or {} server_base_url = data.server_base_url - result = run_openapi_operations( - workflow_doc, swagger_json, data.text, headers, server_base_url - ) + result = "" + error = None - return {"response": result} + try: + result = run_openapi_operations( + workflow_doc, swagger_json, data.text, headers, server_base_url + ) + except Exception as e: + logging.error("[OpenCopilot] Custom planner failed: %s", e) + error = str(e) + + try: + result = create_and_run_openapi_agent(swagger_json, data.text, headers) + except Exception as e: + logging.error("[OpenCopilot] Hierarchical planner failed: %s", e) + error = str(e) + raise + + output = {"response": result if not error else "", "error": error} + + logging.info("[OpenCopilot] Workflow output %s", json.dumps(output)) + + return output