"""Utils for plugins (loading and more to add)"""
import importlib
import json
import os
import sys
from collections import defaultdict
from typing import Any
import yaml
from tqdm import tqdm

from real_agents.adapters.data_model import APIYamlModel, SpecModel
from real_agents.plugins_agent.plugins.plugin_names import PluginName

CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))

AI_PLUGIN_FILE = "ai-plugin.json"
PLUGIN_SPEC_FILE = "openapi.yaml"
PATH_FOLDER = "paths"

def _load_module(name: str, file_path: str) -> Any:
    spec = importlib.util.spec_from_file_location(name, file_path)
    ret = importlib.util.module_from_spec(spec)
    sys.modules[name] = ret
    spec.loader.exec_module(ret)
    return ret

def load_plugin_elements_by_name(plugin_name: str):
    # Check if the plugin name is valid
    assert plugin_name in PluginName.__members__, "Unknown plugin name {}.".format(plugin_name)
    plugin_dir_name = PluginName[plugin_name].value

    # Load in the plugin meta info
    plugin_file_path = os.path.join(CURRENT_PATH, plugin_dir_name)
    data_model_file_path = os.path.join(CURRENT_PATH, "..", "data_model", "plugin", plugin_dir_name)

    meta_info_path = os.path.join(plugin_file_path, AI_PLUGIN_FILE)
    assert os.path.exists(meta_info_path), f"Missing file {meta_info_path} that contains meta info for {plugin_name}"
    with open(meta_info_path, "r") as f:
        meta_info = json.load(f)

        # Load all supported endpoints
    tmp = _load_module(plugin_dir_name, os.path.join(plugin_file_path, "paths", "__init__.py"))
    assert hasattr(tmp, "path_dict"), f"Missing variable path_dict in __init__.py"

    # Load in the plugin spec
    yaml_path = os.path.join(plugin_file_path, PLUGIN_SPEC_FILE)
    assert os.path.exists(yaml_path), f"Missing file: {yaml_path}"

    # fixme: Ugly here, change the whole logic of SpecModel and APIYamlModel to refactor this
    # Load yaml from yaml_path
    openapi_yaml_json = APIYamlModel.from_yaml(yaml_path).to_json()
    if sorted(list(openapi_yaml_json["paths"].keys())) != sorted(list(tmp.path_dict.values())):
        print(f"{yaml_path} and {plugin_dir_name}/paths/__init__.py do not match. Load the later.")
        # Create a new yaml file with only the endpoints in path_dict
        openapi_yaml_json["paths"] = {
            path: openapi_yaml_json["paths"][path] for path in tmp.path_dict.values()
        }
        new_yaml_file_path = os.path.join(plugin_file_path, "tmp", "openapi.yaml")
        os.makedirs(os.path.dirname(new_yaml_file_path), exist_ok=True)
        with open(new_yaml_file_path, "w") as f:
            # Save in the new yaml file, in yaml format
            yaml.safe_dump(openapi_yaml_json, f, sort_keys=False)
        yaml_path = new_yaml_file_path

    spec_model = SpecModel(yaml_path)
    description = spec_model.full_spec["info"]["description"] if "description" in spec_model.full_spec[
        "info"] else "No description."

    filename2endpoint = tmp.path_dict
    endpoint2caller = {}
    endpoint2output_model = defaultdict(lambda x: x)
    for fn, ep in filename2endpoint.items():
        # load api callers for different endpoints
        tmp = _load_module(f"{plugin_dir_name}:{fn}:caller", os.path.join(plugin_file_path, "paths", fn + ".py"))
        assert hasattr(tmp, "call_api"), f"Missing function call_api in {fn}.py"
        endpoint2caller[ep] = tmp.call_api

        # load output data model for different endpoints
        data_model_path = os.path.join(data_model_file_path, fn + ".py")
        if not os.path.exists(data_model_path):
            output_model = lambda x: x
        else:
            tmp = _load_module(f"{plugin_dir_name}:{fn}:output_model", data_model_path)
            assert hasattr(tmp, "convert"), f"Missing function convert in {fn}.py"
            output_model = tmp.convert
        endpoint2output_model[ep] = output_model

    need_auth = meta_info["manifest"]["auth"]["type"] not in [None, "None", "none", "Null", "null"]

    return {
        "name": plugin_name,
        "description": description,
        "meta_info": meta_info,
        "spec_model": spec_model,
        "endpoint2caller": endpoint2caller,
        "endpoint2output_model": endpoint2output_model,
        "need_auth": need_auth,
    }

def load_all_plugins_elements():
    all_plugins_elements = {}

    for plugin_name in tqdm(PluginName.__members__):
        try:
            all_plugins_elements[plugin_name] = load_plugin_elements_by_name(plugin_name)
        except Exception as e:
            print(f"Error when loading plugin {plugin_name}: {e}")

    return all_plugins_elements


"""Utils for plugins (loading and more to add)"""
import importlib
import json
import os
import sys
from collections import defaultdict
from typing import Any
import yaml
from tqdm import tqdm

from real_agents.adapters.data_model import APIYamlModel, SpecModel
from real_agents.plugins_agent.plugins.plugin_names import PluginName

CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))

AI_PLUGIN_FILE = "ai-plugin.json"
PLUGIN_SPEC_FILE = "openapi.yaml"
PATH_FOLDER = "paths"

def _load_module(name: str, file_path: str) -> Any:
    spec = importlib.util.spec_from_file_location(name, file_path)
    ret = importlib.util.module_from_spec(spec)
    sys.modules[name] = ret
    spec.loader.exec_module(ret)
    return ret

def load_plugin_elements_by_name(plugin_name: str):
    # Check if the plugin name is valid
    assert plugin_name in PluginName.__members__, "Unknown plugin name {}.".format(plugin_name)
    plugin_dir_name = PluginName[plugin_name].value

    # Load in the plugin meta info
    plugin_file_path = os.path.join(CURRENT_PATH, plugin_dir_name)
    data_model_file_path = os.path.join(CURRENT_PATH, "..", "data_model", "plugin", plugin_dir_name)

    meta_info_path = os.path.join(plugin_file_path, AI_PLUGIN_FILE)
    assert os.path.exists(meta_info_path), f"Missing file {meta_info_path} that contains meta info for {plugin_name}"
    with open(meta_info_path, "r") as f:
        meta_info = json.load(f)

        # Load all supported endpoints
    tmp = _load_module(plugin_dir_name, os.path.join(plugin_file_path, "paths", "__init__.py"))
    assert hasattr(tmp, "path_dict"), f"Missing variable path_dict in __init__.py"


from tqdm import tqdm

from real_agents.adapters.data_model import APIYamlModel, SpecModel
from real_agents.plugins_agent.plugins.plugin_names import PluginName

CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))

AI_PLUGIN_FILE = "ai-plugin.json"
PLUGIN_SPEC_FILE = "openapi.yaml"
PATH_FOLDER = "paths"

def _load_module(name: str, file_path: str) -> Any:
    spec = importlib.util.spec_from_file_location(name, file_path)
    ret = importlib.util.module_from_spec(spec)
    sys.modules[name] = ret
    spec.loader.exec_module(ret)
    return ret

def load_plugin_elements_by_name(plugin_name: str):
    # Check if the plugin name is valid
    assert plugin_name in PluginName.__members__, "Unknown plugin name {}.".format(plugin_name)
    plugin_dir_name = PluginName[plugin_name].value

    # Load in the plugin meta info
    plugin_file_path = os.path.join(CURRENT_PATH, plugin_dir_name)
    data_model_file_path = os.path.join(CURRENT_PATH, "..", "data_model", "plugin", plugin_dir_name)

    meta_info_path = os.path.join(plugin_file_path, AI_PLUGIN_FILE)
    assert os.path.exists(meta_info_path), f"Missing file {meta_info_path} that contains meta info for {plugin_name}"
    with open(meta_info_path, "r") as f:
        meta_info = json.load(f)

        # Load all supported endpoints
    tmp = _load_module(plugin_dir_name, os.path.join(plugin_file_path, "paths", "__init__.py"))
    assert hasattr(tmp, "path_dict"), f"Missing variable path_dict in __init__.py"

    # Load in the plugin spec
    yaml_path = os.path.join(plugin_file_path, PLUGIN_SPEC_FILE)
    assert os.path.exists(yaml_path), f"Missing file: {yaml_path}"

    # fixme: Ugly here, change the whole logic of SpecModel and APIYamlModel to refactor this
    # Load yaml from yaml_path
    openapi_yaml_json = APIYamlModel.from_yaml(yaml_path).to_json()
    if sorted(list(openapi_yaml_json["paths"].keys())) != sorted(list(tmp.path_dict.values())):
        print(f"{yaml_path} and {plugin_dir_name}/paths/__init__.py do not match. Load the later.")
        # Create a new yaml file with only the endpoints in path_dict
        openapi_yaml_json["paths"] = {
            path: openapi_yaml_json["paths"][path] for path in tmp.path_dict.values()
        }
        new_yaml_file_path = os.path.join(plugin_file_path, "tmp", "openapi.yaml")
        os.makedirs(os.path.dirname(new_yaml_file_path), exist_ok=True)
        with open(new_yaml_file_path, "w") as f:
            # Save in the new yaml file, in yaml format
            yaml.safe_dump(openapi_yaml_json, f, sort_keys=False)
        yaml_path = new_yaml_file_path

    spec_model = SpecModel(yaml_path)
    description = spec_model.full_spec["info"]["description"] if "description" in spec_model.full_spec[
        "info"] else "No description."

    filename2endpoint = tmp.path_dict
    endpoint2caller = {}
    endpoint2output_model = defaultdict(lambda x: x)
    for fn, ep in filename2endpoint.items():
        # load api callers for different endpoints
        tmp = _load_module(f"{plugin_dir_name}:{fn}:caller", os.path.join(plugin_file_path, "paths", fn + ".py"))
        assert hasattr(tmp, "call_api"), f"Missing function call_api in {fn}.py"
        endpoint2caller[ep] = tmp.call_api

        # load output data model for different endpoints
        data_model_path = os.path.join(data_model_file_path, fn + ".py")
        if not os.path.exists(data_model_path):
            output_model = lambda x: x
        else:
            tmp = _load_module(f"{plugin_dir_name}:{fn}:output_model", data_model_path)
            assert hasattr(tmp, "convert"), f"Missing function convert in {fn}.py"
            output_model = tmp.convert
        endpoint2output_model[ep] = output_model

    need_auth = meta_info["manifest"]["auth"]["type"] not in [None, "None", "none", "Null", "null"]

    return {
        "name": plugin_name,
        "description": description,
        "meta_info": meta_info,
        "spec_model": spec_model,
        "endpoint2caller": endpoint2caller,
        "endpoint2output_model": endpoint2output_model,
        "need_auth": need_auth,
    }

def load_all_plugins_elements():
    all_plugins_elements = {}

    for plugin_name in tqdm(PluginName.__members__):
        try:
            all_plugins_elements[plugin_name] = load_plugin_elements_by_name(plugin_name)
        except Exception as e:
            print(f"Error when loading plugin {plugin_name}: {e}")

    return all_plugins_elements


            output_model = lambda x: x
        else:
            tmp = _load_module(f"{plugin_dir_name}:{fn}:output_model", data_model_path)
            assert hasattr(tmp, "convert"), f"Missing function convert in {fn}.py"
            output_model = tmp.convert
        endpoint2output_model[ep] = output_model

    need_auth = meta_info["manifest"]["auth"]["type"] not in [None, "None", "none", "Null", "null"]

    return {
        "name": plugin_name,
        "description": description,
        "meta_info": meta_info,
        "spec_model": spec_model,
        "endpoint2caller": endpoint2caller,
        "endpoint2output_model": endpoint2output_model,
        "need_auth": need_auth,
    }

def load_all_plugins_elements():
    all_plugins_elements = {}

    for plugin_name in tqdm(PluginName.__members__):
        try:
            all_plugins_elements[plugin_name] = load_plugin_elements_by_name(plugin_name)
        except Exception as e:
            print(f"Error when loading plugin {plugin_name}: {e}")

    return all_plugins_elements


"""Implementation of Tool Selector that automate the selection of tools for the question (or sub-question)."""
import os
import pickle

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from tqdm import tqdm
from typing import Any

from real_agents.adapters.data_model import SpecModel
from langchain.embeddings.huggingface import HuggingFaceInstructEmbeddings

DEFAULT_TOOL_INSTRUCTION = "Represent the tool description for retrieval:"
DEFAULT_QUERY_INSTRUCTION = "Represent the question for retrieving tools that can be used to solve the question:"
PLUGIN_SPEC_FILE = "openapi.yaml"
CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))
EMBEDDING_CACHE_PATH = os.path.join(CURRENT_PATH, "..", "..", "..", "backend", "static", "tool_embeddings")
if not os.path.exists(EMBEDDING_CACHE_PATH):
    os.makedirs(EMBEDDING_CACHE_PATH)

class ToolSelector:
    """
    This class is used to select the appropriate tool list for the question.
    """

    # add valid mode here if needed
    valid_modes = ["embedding"]

    """
    Example:
        .. code-block:: python
            mode_args = {"embedding": HuggingFaceInstructEmbeddings, "model_name": "hkunlp/instructor-large", "embed_instruction": "Represent the tool description for retrieval:", "query_instruction": "Represent the question for retrieving tools that can be used to solve the question:"}
            tool_selector = ToolSelector(mode="embedding", mode_args=mode_args)
            model_name = "hkunlp/instructor-large"
            model_kwargs = {'device': 'cpu'}
            hf = HuggingFaceInstructEmbeddings(
                model_name=model_name, model_kwargs=model_kwargs
            )
    """
    user_id: str = None
    chat_id: str = None

    def __init__(self, tools_list: list = [], mode: str = "embedding", mode_args=None, api_key_pool: Any = None):
        """
        Initialize the tool selector.
        """
        if mode_args is None:
            mode_args = {}
        if mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{mode}'. Valid modes are {self.valid_modes}")
        self.tool_paths = [
            plugin_file_path
            for plugin_file_path in os.listdir(CURRENT_PATH)
            if ".py" not in plugin_file_path
               and plugin_file_path != "_scripts"
               and plugin_file_path != "__pycache__"
               and plugin_file_path != "README.md"
               and plugin_file_path != "descriptions.json"
        ]
        self.tool_list = tools_list
        self.mode = mode
        self.api_key_pool = api_key_pool
        if mode == "embedding":
            self._init_embedding(mode_args)
        else:
            raise ValueError(f"Unhandled mode '{mode}'.")

    def _init_embedding(self, mode_args: dict):
        embedding = mode_args.get("embedding", HuggingFaceInstructEmbeddings)
        if embedding == HuggingFaceInstructEmbeddings:
            model_name = mode_args.get("model_name", "hkunlp/instructor-large")
            embed_instruction = mode_args.get("embed_instruction", DEFAULT_TOOL_INSTRUCTION)
            query_instruction = mode_args.get("query_instruction", DEFAULT_QUERY_INSTRUCTION)
            self.embedding = HuggingFaceInstructEmbeddings(
                model_name=model_name, embed_instruction=embed_instruction, query_instruction=query_instruction
            )

    def get_tool_descriptions(self) -> list:
        """
        Get the tool descriptions.
        """
        descriptions = []
        tool_paths = self.tool_paths
        yaml_paths = [os.path.join(CURRENT_PATH, tool_name, PLUGIN_SPEC_FILE) for tool_name in tool_paths]
        for yaml_path, plugin_file_path in tqdm(zip(yaml_paths, tool_paths), total=len(yaml_paths)):
            if os.path.isdir(os.path.join(CURRENT_PATH, plugin_file_path)):
                retrieved = False
                try:
                    spec_model = SpecModel(yaml_path)
                    retrieved = True
                except:
                    print("Error loading yaml", yaml_path)
                if not retrieved:
                    description = "No description."
                else:
                    description = (
                        spec_model.full_spec["info"]["description"] if "description" in spec_model.full_spec[
                            "info"] else "No description."
                    )
                descriptions.append(description)
        return descriptions

    def get_api_key_from_tool_name(self, tool_name: str) -> str:
        """
        Get the API key from the tool name.
        """
        user_id = self.user_id
        api_key_info = self.api_key_pool.get_pool_info_with_id(user_id, default_value=[])
        if len([i for i in api_key_info if i["tool_name"] == tool_name]) != 0:
            api_key = [i for i in api_key_info if i["tool_name"] == tool_name][0]["api_key"]
        else:
            api_key = None
        return api_key

    def check_plugin_valid(self, tool_path: str) -> bool:
        """
        Check if the plugin is valid. Return false if this plugin requires an API key but the user has not provided one or plugin not found.
        """
        plugins = self.tool_list
        # check if plugin exists and get the plugin if it exists
        if len([i for i in plugins if i["name"].lower() == tool_path.lower()]) != 0:
            plugin = [i for i in plugins if i["name"].lower() == tool_path.lower()][0]
        else:
            plugin = None
            print(f"Plugin {tool_path} not found.")

        # check if plugin requires an API key and if the user has provided one
        if plugin is not None:
            if plugin["require_api_key"] and self.get_api_key_from_tool_name(tool_path) == None:
                return False
            else:
                return True
        else:
            return False

    def load_query_from_message_list(self, message_list: list[dict[str, str]], user_intent: str) -> str:
        """
        Load the query from the message list.
        """

        """
        Example:
        message_list = [{'message_type': 'human_message', 'message_content': 'buy nike shoes', 'message_id': 362, 'parent_message_id': -1}, {'message_type': 'ai_message', 'message_content': '', 'message_id': 363, 'parent_message_id': 362}]
        """
        # concatenate all history messages into one single query
        # The message_list is the history message list so we need to concatenate user intent(current message) to the end of the message list
        query = ""
        for message in message_list:
            # only concatenate human messages since we only need to retrieve tools based on user intent and the ai_message can be long sometimes which will influence the embedding
            if "message_content" in message.keys() and "message_type" in message.keys() and message[
                "message_type"] == 'human_message':
                query += (message["message_content"] + " ")
            else:
                continue
        query += user_intent
        return query

    def select_tools(self, query: str = "", top_k: int = 8):
        """
        Select the top k tools based on the similarity between the query and the tool description.
        """
        if query == "":
            raise ValueError("Query cannot be empty.")
        if self.mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{self.mode}'. Valid modes are {self.valid_modes}")

        if self.mode == "embedding":
            return self._select_tools_embedding(query, top_k)
        else:
            raise ValueError(f"Unhandled mode '{self.mode}'.")

    def _select_tools_embedding(self, query: str, top_k: int) -> list[str]:
        embedding = self.embedding
        # check if the embedding is InstructorEmbeddings
        if isinstance(self.embedding, HuggingFaceInstructEmbeddings):
            tool_embeddings = []
            for name, description in zip(self.tool_paths, self.get_tool_descriptions()):
                # Define file path for the cached embedding
                tool_embedding_file = EMBEDDING_CACHE_PATH + "/" + name + ".pkl"
                # Check if tool embedding is already cached
                if os.path.isfile(tool_embedding_file):
                    with open(tool_embedding_file, "rb") as f:
                        tool_embedding = pickle.load(f)
                # no cached embedding, compute and cache it
                else:
                    tool_embedding = embedding.embed_documents([description])
                    with open(tool_embedding_file, "wb") as f:
                        pickle.dump(tool_embedding, f)
                tool_embeddings.extend(tool_embedding)

            query_embeddings = [embedding.embed_query(query)]

            similarities = cosine_similarity(query_embeddings, tool_embeddings)

            # eliminate invalid plugins
            for idx, tool_path in enumerate(self.tool_paths):
                if not self.check_plugin_valid(tool_path):
                    similarities[0][idx] = -1

            # get indices of top k similarities
            top_k_indices = np.argsort(similarities.flatten())[-top_k:]

            top_k_indices = top_k_indices.tolist()

            # return upper case tool names since tool id is the upper case of its name
            return [tool_name.upper() for idx, tool_name in enumerate(self.tool_paths) if idx in top_k_indices]


ToolSelector

import os
import pickle

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from tqdm import tqdm
from typing import Any

from real_agents.adapters.data_model import SpecModel
from langchain.embeddings.huggingface import HuggingFaceInstructEmbeddings

DEFAULT_TOOL_INSTRUCTION = "Represent the tool description for retrieval:"
DEFAULT_QUERY_INSTRUCTION = "Represent the question for retrieving tools that can be used to solve the question:"
PLUGIN_SPEC_FILE = "openapi.yaml"
CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))
EMBEDDING_CACHE_PATH = os.path.join(CURRENT_PATH, "..", "..", "..", "backend", "static", "tool_embeddings")
if not os.path.exists(EMBEDDING_CACHE_PATH):
    os.makedirs(EMBEDDING_CACHE_PATH)

class ToolSelector:
    """
    This class is used to select the appropriate tool list for the question.
    """

    # add valid mode here if needed
    valid_modes = ["embedding"]

    """
    Example:
        .. code-block:: python
            mode_args = {"embedding": HuggingFaceInstructEmbeddings, "model_name": "hkunlp/instructor-large", "embed_instruction": "Represent the tool description for retrieval:", "query_instruction": "Represent the question for retrieving tools that can be used to solve the question:"}
            tool_selector = ToolSelector(mode="embedding", mode_args=mode_args)
            model_name = "hkunlp/instructor-large"
            model_kwargs = {'device': 'cpu'}
            hf = HuggingFaceInstructEmbeddings(
                model_name=model_name, model_kwargs=model_kwargs
            )
    """
    user_id: str = None
    chat_id: str = None

    def __init__(self, tools_list: list = [], mode: str = "embedding", mode_args=None, api_key_pool: Any = None):
        """
        Initialize the tool selector.
        """
        if mode_args is None:
            mode_args = {}
        if mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{mode}'. Valid modes are {self.valid_modes}")
        self.tool_paths = [
            plugin_file_path
            for plugin_file_path in os.listdir(CURRENT_PATH)
            if ".py" not in plugin_file_path
               and plugin_file_path != "_scripts"
               and plugin_file_path != "__pycache__"
               and plugin_file_path != "README.md"
               and plugin_file_path != "descriptions.json"
        ]
        self.tool_list = tools_list
        self.mode = mode
        self.api_key_pool = api_key_pool
        if mode == "embedding":
            self._init_embedding(mode_args)
        else:
            raise ValueError(f"Unhandled mode '{mode}'.")

    def _init_embedding(self, mode_args: dict):
        embedding = mode_args.get("embedding", HuggingFaceInstructEmbeddings)
        if embedding == HuggingFaceInstructEmbeddings:
            model_name = mode_args.get("model_name", "hkunlp/instructor-large")
            embed_instruction = mode_args.get("embed_instruction", DEFAULT_TOOL_INSTRUCTION)
            query_instruction = mode_args.get("query_instruction", DEFAULT_QUERY_INSTRUCTION)
            self.embedding = HuggingFaceInstructEmbeddings(
                model_name=model_name, embed_instruction=embed_instruction, query_instruction=query_instruction
            )

    def get_tool_descriptions(self) -> list:
        """
        Get the tool descriptions.
        """
        descriptions = []
        tool_paths = self.tool_paths
        yaml_paths = [os.path.join(CURRENT_PATH, tool_name, PLUGIN_SPEC_FILE) for tool_name in tool_paths]
        for yaml_path, plugin_file_path in tqdm(zip(yaml_paths, tool_paths), total=len(yaml_paths)):
            if os.path.isdir(os.path.join(CURRENT_PATH, plugin_file_path)):
                retrieved = False
                try:
                    spec_model = SpecModel(yaml_path)
                    retrieved = True
                except:
                    print("Error loading yaml", yaml_path)
                if not retrieved:
                    description = "No description."
                else:
                    description = (
                        spec_model.full_spec["info"]["description"] if "description" in spec_model.full_spec[
                            "info"] else "No description."
                    )
                descriptions.append(description)
        return descriptions

    def get_api_key_from_tool_name(self, tool_name: str) -> str:
        """
        Get the API key from the tool name.
        """
        user_id = self.user_id
        api_key_info = self.api_key_pool.get_pool_info_with_id(user_id, default_value=[])
        if len([i for i in api_key_info if i["tool_name"] == tool_name]) != 0:
            api_key = [i for i in api_key_info if i["tool_name"] == tool_name][0]["api_key"]
        else:
            api_key = None
        return api_key

    def check_plugin_valid(self, tool_path: str) -> bool:
        """
        Check if the plugin is valid. Return false if this plugin requires an API key but the user has not provided one or plugin not found.
        """
        plugins = self.tool_list
        # check if plugin exists and get the plugin if it exists
        if len([i for i in plugins if i["name"].lower() == tool_path.lower()]) != 0:
            plugin = [i for i in plugins if i["name"].lower() == tool_path.lower()][0]
        else:
            plugin = None
            print(f"Plugin {tool_path} not found.")

        # check if plugin requires an API key and if the user has provided one
        if plugin is not None:
            if plugin["require_api_key"] and self.get_api_key_from_tool_name(tool_path) == None:
                return False
            else:
                return True
        else:
            return False

    def load_query_from_message_list(self, message_list: list[dict[str, str]], user_intent: str) -> str:
        """
        Load the query from the message list.
        """

        """
        Example:
        message_list = [{'message_type': 'human_message', 'message_content': 'buy nike shoes', 'message_id': 362, 'parent_message_id': -1}, {'message_type': 'ai_message', 'message_content': '', 'message_id': 363, 'parent_message_id': 362}]
        """
        # concatenate all history messages into one single query
        # The message_list is the history message list so we need to concatenate user intent(current message) to the end of the message list
        query = ""
        for message in message_list:
            # only concatenate human messages since we only need to retrieve tools based on user intent and the ai_message can be long sometimes which will influence the embedding
            if "message_content" in message.keys() and "message_type" in message.keys() and message[
                "message_type"] == 'human_message':
                query += (message["message_content"] + " ")
            else:
                continue
        query += user_intent
        return query

    def select_tools(self, query: str = "", top_k: int = 8):
        """
        Select the top k tools based on the similarity between the query and the tool description.
        """
        if query == "":
            raise ValueError("Query cannot be empty.")
        if self.mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{self.mode}'. Valid modes are {self.valid_modes}")

        if self.mode == "embedding":
            return self._select_tools_embedding(query, top_k)
        else:
            raise ValueError(f"Unhandled mode '{self.mode}'.")

    def _select_tools_embedding(self, query: str, top_k: int) -> list[str]:
        embedding = self.embedding
        # check if the embedding is InstructorEmbeddings
        if isinstance(self.embedding, HuggingFaceInstructEmbeddings):
            tool_embeddings = []
            for name, description in zip(self.tool_paths, self.get_tool_descriptions()):
                # Define file path for the cached embedding
                tool_embedding_file = EMBEDDING_CACHE_PATH + "/" + name + ".pkl"
                # Check if tool embedding is already cached
                if os.path.isfile(tool_embedding_file):
                    with open(tool_embedding_file, "rb") as f:
                        tool_embedding = pickle.load(f)
                # no cached embedding, compute and cache it
                else:
                    tool_embedding = embedding.embed_documents([description])
                    with open(tool_embedding_file, "wb") as f:
                        pickle.dump(tool_embedding, f)
                tool_embeddings.extend(tool_embedding)

            query_embeddings = [embedding.embed_query(query)]

            similarities = cosine_similarity(query_embeddings, tool_embeddings)

            # eliminate invalid plugins
            for idx, tool_path in enumerate(self.tool_paths):
                if not self.check_plugin_valid(tool_path):
                    similarities[0][idx] = -1

            # get indices of top k similarities
            top_k_indices = np.argsort(similarities.flatten())[-top_k:]

            top_k_indices = top_k_indices.tolist()

            # return upper case tool names since tool id is the upper case of its name
            return [tool_name.upper() for idx, tool_name in enumerate(self.tool_paths) if idx in top_k_indices]


    This class is used to select the appropriate tool list for the question.
    """

    # add valid mode here if needed
    valid_modes = ["embedding"]

    """
    Example:
        .. code-block:: python
            mode_args = {"embedding": HuggingFaceInstructEmbeddings, "model_name": "hkunlp/instructor-large", "embed_instruction": "Represent the tool description for retrieval:", "query_instruction": "Represent the question for retrieving tools that can be used to solve the question:"}
            tool_selector = ToolSelector(mode="embedding", mode_args=mode_args)
            model_name = "hkunlp/instructor-large"
            model_kwargs = {'device': 'cpu'}
            hf = HuggingFaceInstructEmbeddings(
                model_name=model_name, model_kwargs=model_kwargs
            )
    """
    user_id: str = None
    chat_id: str = None

    def __init__(self, tools_list: list = [], mode: str = "embedding", mode_args=None, api_key_pool: Any = None):
        """
        Initialize the tool selector.
        """
        if mode_args is None:
            mode_args = {}
        if mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{mode}'. Valid modes are {self.valid_modes}")
        self.tool_paths = [
            plugin_file_path
            for plugin_file_path in os.listdir(CURRENT_PATH)
            if ".py" not in plugin_file_path
               and plugin_file_path != "_scripts"
               and plugin_file_path != "__pycache__"
               and plugin_file_path != "README.md"
               and plugin_file_path != "descriptions.json"
        ]
        self.tool_list = tools_list
        self.mode = mode
        self.api_key_pool = api_key_pool
        if mode == "embedding":
            self._init_embedding(mode_args)
        else:
            raise ValueError(f"Unhandled mode '{mode}'.")

    def _init_embedding(self, mode_args: dict):
        embedding = mode_args.get("embedding", HuggingFaceInstructEmbeddings)
        if embedding == HuggingFaceInstructEmbeddings:
            model_name = mode_args.get("model_name", "hkunlp/instructor-large")
            embed_instruction = mode_args.get("embed_instruction", DEFAULT_TOOL_INSTRUCTION)
            query_instruction = mode_args.get("query_instruction", DEFAULT_QUERY_INSTRUCTION)
            self.embedding = HuggingFaceInstructEmbeddings(
                model_name=model_name, embed_instruction=embed_instruction, query_instruction=query_instruction
            )

    def get_tool_descriptions(self) -> list:
        """
        Get the tool descriptions.
        """
        descriptions = []
        tool_paths = self.tool_paths
        yaml_paths = [os.path.join(CURRENT_PATH, tool_name, PLUGIN_SPEC_FILE) for tool_name in tool_paths]
        for yaml_path, plugin_file_path in tqdm(zip(yaml_paths, tool_paths), total=len(yaml_paths)):
            if os.path.isdir(os.path.join(CURRENT_PATH, plugin_file_path)):


            mode_args = {}
        if mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{mode}'. Valid modes are {self.valid_modes}")
        self.tool_paths = [
            plugin_file_path
            for plugin_file_path in os.listdir(CURRENT_PATH)
            if ".py" not in plugin_file_path
               and plugin_file_path != "_scripts"
               and plugin_file_path != "__pycache__"
               and plugin_file_path != "README.md"
               and plugin_file_path != "descriptions.json"
        ]
        self.tool_list = tools_list
        self.mode = mode
        self.api_key_pool = api_key_pool
        if mode == "embedding":
            self._init_embedding(mode_args)
        else:
            raise ValueError(f"Unhandled mode '{mode}'.")

    def _init_embedding(self, mode_args: dict):
        embedding = mode_args.get("embedding", HuggingFaceInstructEmbeddings)
        if embedding == HuggingFaceInstructEmbeddings:
            model_name = mode_args.get("model_name", "hkunlp/instructor-large")
            embed_instruction = mode_args.get("embed_instruction", DEFAULT_TOOL_INSTRUCTION)
            query_instruction = mode_args.get("query_instruction", DEFAULT_QUERY_INSTRUCTION)
            self.embedding = HuggingFaceInstructEmbeddings(
                model_name=model_name, embed_instruction=embed_instruction, query_instruction=query_instruction
            )

    def get_tool_descriptions(self) -> list:
        """
        Get the tool descriptions.
        """
        descriptions = []
        tool_paths = self.tool_paths
        yaml_paths = [os.path.join(CURRENT_PATH, tool_name, PLUGIN_SPEC_FILE) for tool_name in tool_paths]
        for yaml_path, plugin_file_path in tqdm(zip(yaml_paths, tool_paths), total=len(yaml_paths)):
            if os.path.isdir(os.path.join(CURRENT_PATH, plugin_file_path)):
                retrieved = False
                try:
                    spec_model = SpecModel(yaml_path)
                    retrieved = True
                except:
                    print("Error loading yaml", yaml_path)
                if not retrieved:
                    description = "No description."
                else:
                    description = (


               and plugin_file_path != "descriptions.json"
        ]
        self.tool_list = tools_list
        self.mode = mode
        self.api_key_pool = api_key_pool
        if mode == "embedding":
            self._init_embedding(mode_args)
        else:
            raise ValueError(f"Unhandled mode '{mode}'.")

    def _init_embedding(self, mode_args: dict):
        embedding = mode_args.get("embedding", HuggingFaceInstructEmbeddings)
        if embedding == HuggingFaceInstructEmbeddings:
            model_name = mode_args.get("model_name", "hkunlp/instructor-large")
            embed_instruction = mode_args.get("embed_instruction", DEFAULT_TOOL_INSTRUCTION)
            query_instruction = mode_args.get("query_instruction", DEFAULT_QUERY_INSTRUCTION)
            self.embedding = HuggingFaceInstructEmbeddings(
                model_name=model_name, embed_instruction=embed_instruction, query_instruction=query_instruction
            )

    def get_tool_descriptions(self) -> list:
        """
        Get the tool descriptions.
        """
        descriptions = []
        tool_paths = self.tool_paths
        yaml_paths = [os.path.join(CURRENT_PATH, tool_name, PLUGIN_SPEC_FILE) for tool_name in tool_paths]
        for yaml_path, plugin_file_path in tqdm(zip(yaml_paths, tool_paths), total=len(yaml_paths)):
            if os.path.isdir(os.path.join(CURRENT_PATH, plugin_file_path)):
                retrieved = False
                try:
                    spec_model = SpecModel(yaml_path)
                    retrieved = True
                except:
                    print("Error loading yaml", yaml_path)
                if not retrieved:
                    description = "No description."
                else:
                    description = (
                        spec_model.full_spec["info"]["description"] if "description" in spec_model.full_spec[
                            "info"] else "No description."
                    )
                descriptions.append(description)
        return descriptions

    def get_api_key_from_tool_name(self, tool_name: str) -> str:
        """
        Get the API key from the tool name.
        """
        user_id = self.user_id
        api_key_info = self.api_key_pool.get_pool_info_with_id(user_id, default_value=[])
        if len([i for i in api_key_info if i["tool_name"] == tool_name]) != 0:
            api_key = [i for i in api_key_info if i["tool_name"] == tool_name][0]["api_key"]
        else:
            api_key = None
        return api_key

    def check_plugin_valid(self, tool_path: str) -> bool:
        """
        Check if the plugin is valid. Return false if this plugin requires an API key but the user has not provided one or plugin not found.
        """
        plugins = self.tool_list
        # check if plugin exists and get the plugin if it exists
        if len([i for i in plugins if i["name"].lower() == tool_path.lower()]) != 0:


        tool_paths = self.tool_paths
        yaml_paths = [os.path.join(CURRENT_PATH, tool_name, PLUGIN_SPEC_FILE) for tool_name in tool_paths]
        for yaml_path, plugin_file_path in tqdm(zip(yaml_paths, tool_paths), total=len(yaml_paths)):
            if os.path.isdir(os.path.join(CURRENT_PATH, plugin_file_path)):
                retrieved = False
                try:
                    spec_model = SpecModel(yaml_path)
                    retrieved = True
                except:
                    print("Error loading yaml", yaml_path)
                if not retrieved:
                    description = "No description."
                else:
                    description = (
                        spec_model.full_spec["info"]["description"] if "description" in spec_model.full_spec[
                            "info"] else "No description."
                    )
                descriptions.append(description)
        return descriptions

    def get_api_key_from_tool_name(self, tool_name: str) -> str:
        """
        Get the API key from the tool name.
        """
        user_id = self.user_id
        api_key_info = self.api_key_pool.get_pool_info_with_id(user_id, default_value=[])
        if len([i for i in api_key_info if i["tool_name"] == tool_name]) != 0:
            api_key = [i for i in api_key_info if i["tool_name"] == tool_name][0]["api_key"]
        else:
            api_key = None
        return api_key

    def check_plugin_valid(self, tool_path: str) -> bool:
        """
        Check if the plugin is valid. Return false if this plugin requires an API key but the user has not provided one or plugin not found.
        """
        plugins = self.tool_list
        # check if plugin exists and get the plugin if it exists
        if len([i for i in plugins if i["name"].lower() == tool_path.lower()]) != 0:
            plugin = [i for i in plugins if i["name"].lower() == tool_path.lower()][0]
        else:
            plugin = None
            print(f"Plugin {tool_path} not found.")

        # check if plugin requires an API key and if the user has provided one
        if plugin is not None:
            if plugin["require_api_key"] and self.get_api_key_from_tool_name(tool_path) == None:
                return False
            else:
                return True
        else:


                else:
                    description = (
                        spec_model.full_spec["info"]["description"] if "description" in spec_model.full_spec[
                            "info"] else "No description."
                    )
                descriptions.append(description)
        return descriptions

    def get_api_key_from_tool_name(self, tool_name: str) -> str:
        """
        Get the API key from the tool name.
        """
        user_id = self.user_id
        api_key_info = self.api_key_pool.get_pool_info_with_id(user_id, default_value=[])
        if len([i for i in api_key_info if i["tool_name"] == tool_name]) != 0:
            api_key = [i for i in api_key_info if i["tool_name"] == tool_name][0]["api_key"]
        else:
            api_key = None
        return api_key

    def check_plugin_valid(self, tool_path: str) -> bool:
        """
        Check if the plugin is valid. Return false if this plugin requires an API key but the user has not provided one or plugin not found.
        """
        plugins = self.tool_list
        # check if plugin exists and get the plugin if it exists
        if len([i for i in plugins if i["name"].lower() == tool_path.lower()]) != 0:
            plugin = [i for i in plugins if i["name"].lower() == tool_path.lower()][0]
        else:
            plugin = None
            print(f"Plugin {tool_path} not found.")

        # check if plugin requires an API key and if the user has provided one
        if plugin is not None:
            if plugin["require_api_key"] and self.get_api_key_from_tool_name(tool_path) == None:
                return False
            else:
                return True
        else:
            return False

    def load_query_from_message_list(self, message_list: list[dict[str, str]], user_intent: str) -> str:
        """
        Load the query from the message list.
        """

        """
        Example:
        message_list = [{'message_type': 'human_message', 'message_content': 'buy nike shoes', 'message_id': 362, 'parent_message_id': -1}, {'message_type': 'ai_message', 'message_content': '', 'message_id': 363, 'parent_message_id': 362}]
        """
        # concatenate all history messages into one single query
        # The message_list is the history message list so we need to concatenate user intent(current message) to the end of the message list
        query = ""
        for message in message_list:
            # only concatenate human messages since we only need to retrieve tools based on user intent and the ai_message can be long sometimes which will influence the embedding
            if "message_content" in message.keys() and "message_type" in message.keys() and message[
                "message_type"] == 'human_message':
                query += (message["message_content"] + " ")
            else:
                continue


        """
        Check if the plugin is valid. Return false if this plugin requires an API key but the user has not provided one or plugin not found.
        """
        plugins = self.tool_list
        # check if plugin exists and get the plugin if it exists
        if len([i for i in plugins if i["name"].lower() == tool_path.lower()]) != 0:
            plugin = [i for i in plugins if i["name"].lower() == tool_path.lower()][0]
        else:
            plugin = None
            print(f"Plugin {tool_path} not found.")

        # check if plugin requires an API key and if the user has provided one
        if plugin is not None:
            if plugin["require_api_key"] and self.get_api_key_from_tool_name(tool_path) == None:
                return False
            else:
                return True
        else:
            return False

    def load_query_from_message_list(self, message_list: list[dict[str, str]], user_intent: str) -> str:
        """
        Load the query from the message list.
        """

        """
        Example:
        message_list = [{'message_type': 'human_message', 'message_content': 'buy nike shoes', 'message_id': 362, 'parent_message_id': -1}, {'message_type': 'ai_message', 'message_content': '', 'message_id': 363, 'parent_message_id': 362}]
        """
        # concatenate all history messages into one single query
        # The message_list is the history message list so we need to concatenate user intent(current message) to the end of the message list
        query = ""
        for message in message_list:
            # only concatenate human messages since we only need to retrieve tools based on user intent and the ai_message can be long sometimes which will influence the embedding
            if "message_content" in message.keys() and "message_type" in message.keys() and message[
                "message_type"] == 'human_message':
                query += (message["message_content"] + " ")
            else:
                continue
        query += user_intent
        return query

    def select_tools(self, query: str = "", top_k: int = 8):
        """
        Select the top k tools based on the similarity between the query and the tool description.
        """
        if query == "":
            raise ValueError("Query cannot be empty.")
        if self.mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{self.mode}'. Valid modes are {self.valid_modes}")

        if self.mode == "embedding":
            return self._select_tools_embedding(query, top_k)
        else:
            raise ValueError(f"Unhandled mode '{self.mode}'.")

    def _select_tools_embedding(self, query: str, top_k: int) -> list[str]:
        embedding = self.embedding
        # check if the embedding is InstructorEmbeddings
        if isinstance(self.embedding, HuggingFaceInstructEmbeddings):
            tool_embeddings = []


        Load the query from the message list.
        """

        """
        Example:
        message_list = [{'message_type': 'human_message', 'message_content': 'buy nike shoes', 'message_id': 362, 'parent_message_id': -1}, {'message_type': 'ai_message', 'message_content': '', 'message_id': 363, 'parent_message_id': 362}]
        """
        # concatenate all history messages into one single query
        # The message_list is the history message list so we need to concatenate user intent(current message) to the end of the message list
        query = ""
        for message in message_list:
            # only concatenate human messages since we only need to retrieve tools based on user intent and the ai_message can be long sometimes which will influence the embedding
            if "message_content" in message.keys() and "message_type" in message.keys() and message[
                "message_type"] == 'human_message':
                query += (message["message_content"] + " ")
            else:
                continue
        query += user_intent
        return query

    def select_tools(self, query: str = "", top_k: int = 8):
        """
        Select the top k tools based on the similarity between the query and the tool description.
        """
        if query == "":
            raise ValueError("Query cannot be empty.")
        if self.mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{self.mode}'. Valid modes are {self.valid_modes}")

        if self.mode == "embedding":
            return self._select_tools_embedding(query, top_k)
        else:
            raise ValueError(f"Unhandled mode '{self.mode}'.")

    def _select_tools_embedding(self, query: str, top_k: int) -> list[str]:
        embedding = self.embedding
        # check if the embedding is InstructorEmbeddings
        if isinstance(self.embedding, HuggingFaceInstructEmbeddings):
            tool_embeddings = []
            for name, description in zip(self.tool_paths, self.get_tool_descriptions()):
                # Define file path for the cached embedding
                tool_embedding_file = EMBEDDING_CACHE_PATH + "/" + name + ".pkl"
                # Check if tool embedding is already cached
                if os.path.isfile(tool_embedding_file):
                    with open(tool_embedding_file, "rb") as f:
                        tool_embedding = pickle.load(f)
                # no cached embedding, compute and cache it
                else:
                    tool_embedding = embedding.embed_documents([description])
                    with open(tool_embedding_file, "wb") as f:
                        pickle.dump(tool_embedding, f)
                tool_embeddings.extend(tool_embedding)


                query += (message["message_content"] + " ")
            else:
                continue
        query += user_intent
        return query

    def select_tools(self, query: str = "", top_k: int = 8):
        """
        Select the top k tools based on the similarity between the query and the tool description.
        """
        if query == "":
            raise ValueError("Query cannot be empty.")
        if self.mode not in self.valid_modes:
            raise ValueError(f"Invalid mode '{self.mode}'. Valid modes are {self.valid_modes}")

        if self.mode == "embedding":
            return self._select_tools_embedding(query, top_k)
        else:
            raise ValueError(f"Unhandled mode '{self.mode}'.")

    def _select_tools_embedding(self, query: str, top_k: int) -> list[str]:
        embedding = self.embedding
        # check if the embedding is InstructorEmbeddings
        if isinstance(self.embedding, HuggingFaceInstructEmbeddings):
            tool_embeddings = []
            for name, description in zip(self.tool_paths, self.get_tool_descriptions()):
                # Define file path for the cached embedding
                tool_embedding_file = EMBEDDING_CACHE_PATH + "/" + name + ".pkl"
                # Check if tool embedding is already cached
                if os.path.isfile(tool_embedding_file):
                    with open(tool_embedding_file, "rb") as f:
                        tool_embedding = pickle.load(f)
                # no cached embedding, compute and cache it
                else:
                    tool_embedding = embedding.embed_documents([description])
                    with open(tool_embedding_file, "wb") as f:
                        pickle.dump(tool_embedding, f)
                tool_embeddings.extend(tool_embedding)

            query_embeddings = [embedding.embed_query(query)]

            similarities = cosine_similarity(query_embeddings, tool_embeddings)

            # eliminate invalid plugins
            for idx, tool_path in enumerate(self.tool_paths):
                if not self.check_plugin_valid(tool_path):
                    similarities[0][idx] = -1

            # get indices of top k similarities
            top_k_indices = np.argsort(similarities.flatten())[-top_k:]

            top_k_indices = top_k_indices.tolist()

            # return upper case tool names since tool id is the upper case of its name
            return [tool_name.upper() for idx, tool_name in enumerate(self.tool_paths) if idx in top_k_indices]


"""Search for products by keyword, price range, and size."""
from typing import Any, Dict

import requests

url = "https://www.klarna.com/us/shopping/public/openai/v0/products"

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    headers = {"Accept": "application/json"}
    response = requests.get(url, headers=headers, params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}

# input_json = {
#     "q": "nike shoes",
#     "size": 10,
#     "min_price": 50,
#     "max_price": 100
# }


"""Search Coursera API for courses matching a query."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.post("https://www.coursera.org/api/rest/v1/search", json=input_json)
    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""NBA stats API path."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.post("https://nba-gpt-prod.onrender.com/basketball_stats", json=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Download PDF from AskYourPDF API."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.post("https://plugin.askyourpdf.com/api/download_pdf", params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Perform query to AskYourPDF API."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.post("https://plugin.askyourpdf.com/query", json=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""LLM API for Wolfram Alpha"""
import requests

def call_api(input_json, api_key):
    input_json["appid"] = api_key
    response = requests.get("https://www.wolframalpha.com/api/v1/llm-api", params=input_json)

    if response.status_code == 200:
        return response.content.decode("utf-8")
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Cloud plugin for Wolfram Alpha API"""
import requests

def call_api(input_json, api_key):
    input_json["appid"] = api_key
    response = requests.get("https://www.wolframalpha.com/api/v1/cloud-plugin", params=input_json)

    if response.status_code == 200:
        return response.content.decode("utf-8")
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Render diagram path for Show Me plugin."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.get("https://showme.redstarplugin.com/render/", params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Show Carousel path for Show Me plugin."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.get("https://showme.redstarplugin.com/show-carousel", params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Guidelines for diagramming"""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.get("https://showme.redstarplugin.com/diagram-guidelines", params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Radars API path."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    location = input_json['location']
    response = requests.get(f"https://openai-plugin.xweather.com/radar/{location}", params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Weather summary path for XWeather plugin."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    url = "https://openai-plugin.xweather.com/weather/summary/{}".format(input_json['location'])
    response = requests.get(url)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Weather Forecast API Path."""
from typing import Dict, Any
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    location = input_json["location"]
    url = f"https://openai-plugin.xweather.com/weather/forecast/{location}"
    response = requests.get(url)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Search news from Biztoc API."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.get("https://ai.biztoc.com/ai/news", params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Scrape data from a website using the Scraper API."""
import requests
from typing import Any, Dict

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.post("https://scraper.gafo.tech/scrape", json=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Jobsearch API jobs path."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    url = "https://jobsearch.vencio.de/jobs"
    headers = {
        "Content-Type": "application/json"
    }
    response = requests.get(url, headers=headers, params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Maps plugin for generating maps from latlng coordinates."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    query_param = input_json["latlng"]
    response = requests.get(f"https://maps.smoothplugins.com/?latlng={query_param}")

    if response.status_code == 200:
        return {"result": response.content.decode()}
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Explains a foreign phrase in the context of a full query."""
from typing import Any, Dict

import requests

url = "https://api.speak.com/v1/public/openai/explain-phrase"

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    headers = {"Content-Type": "application/json"}
    response = requests.post(url, json=input_json, headers=headers)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}

# input_json = {
#         "foreign_phrase": "no mames",
#         "learning_language": "Spanish",
#         "native_language": "English",
#         "additional_context": "Informal conversation",
#         "full_query": "What does no mames mean in English?"
#     }


"""Translate a phrase from one language to another."""
from typing import Any, Dict

import requests

url = "https://api.speak.com/v1/public/openai/translate"

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    headers = {"Content-Type": "application/json"}
    response = requests.post(url, json=input_json, headers=headers)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}

# input_json = {
#     "phrase_to_translate": "Hello, how are you?",
#     "learning_language": "Spanish",
#     "native_language": "English",
#     "additional_context": "Casual conversation",
#     "full_query": "How do I say hello, how are you in Spanish?"
# }


"""Explain the task to the user."""
from typing import Any, Dict

import requests

url = "https://api.speak.com/v1/public/openai/explain-task"

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    headers = {"Content-Type": "application/json"}
    response = requests.post(url, json=input_json, headers=headers)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}

# input_json = {
#         "task_description": "ask for directions,
#         "learning_language": "French",
#         "native_language": "English",
#         "additional_context": "In a city",
#         "full_query": "How do I ask for directions in French?"
#     }


"""Search for teachers on Outschool."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.get("https://chatgpt-plugin.outschool.com/api/teachers", params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Get classes from Outschool API."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.get("https://chatgpt-plugin.outschool.com/api/classes", params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Data path for DreamInterpreter plugin."""""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    response = requests.post("https://dreamplugin.bgnetmobile.com/api/data", json=input_json)

    if response.status_code == 200:
        return response.content
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Dream Interpreter plugin."""
from typing import Any, Dict
import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    dream_text = input_json["DreamText"]
    response = requests.get(f"https://dreamplugin.bgnetmobile.com/getDream/{dream_text}")
    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""The Zapier plugin personnel openapi.yaml handling, since it is special case, we need to handle it separately"""
import os
import requests

FILE_PATH = os.path.dirname(os.path.abspath(__file__))

# You need to manage your actions first in https://nla.zapier.com/providers/

# Reload the openapi
def reload_openapi(api_key, openapi_json):
    # Original data
    headers = {"X-API-Key": api_key, }
    # Call read the openapi
    url = "https://nla.zapier.com/api/v1/exposed/"

    data = None
    while True:
        try:
            response = requests.get(url, headers=headers)
            data = response.json()
            break
        except Exception as e:
            print(e)
            # if an error occurs, continue to retry
            import time
            time.sleep(5)
            continue

    try:
        data = data['results']
    except Exception as e:
        print(e)
        return openapi_json, {}

    new_paths = {}
    for item in data:
        new_paths['/api/v1/exposed/{}/execute/'.format(item["id"])] = {
            'post': {  # assuming POST method for all operations
                'operationId': item['operation_id'],
                'description': item['description'],
                'parameters': [
                    {'name': k, 'in': 'query', 'required': True, 'schema': {'type': v}}
                    for k, v in item['params'].items()
                ],
                "security": {
                    "SessionAuth": [],
                    "AccessPointApiKeyHeader": [],
                    "AccessPointApiKeyQuery": [],
                    "AccessPointOAuth": []
                }
            }
        }

    openapi_json['paths'] = openapi_json['paths'] | new_paths

    return openapi_json, new_paths

# Reload the endpoints
def reload_endpoints(new_paths):
    new_endpoint2caller = {}
    for new_path in new_paths:
        # create the call function
        def call_api(input_json, api_key):
            import requests
            headers = {"X-API-Key": api_key}
            url = "https://nla.zapier.com" + new_path
            response = requests.post(url, headers=headers, json=input_json)

            if response.status_code == 200:
                return response.json()
            else:
                return response.text

        new_endpoint2caller[new_path] = call_api

    return new_endpoint2caller


"""The Zapier plugin personnel openapi.yaml handling, since it is special case, we need to handle it separately"""
import os
import requests

FILE_PATH = os.path.dirname(os.path.abspath(__file__))

# You need to manage your actions first in https://nla.zapier.com/providers/

# Reload the openapi
def reload_openapi(api_key, openapi_json):
    # Original data
    headers = {"X-API-Key": api_key, }
    # Call read the openapi
    url = "https://nla.zapier.com/api/v1/exposed/"

    data = None
    while True:
        try:
            response = requests.get(url, headers=headers)
            data = response.json()
            break
        except Exception as e:
            print(e)
            # if an error occurs, continue to retry
            import time
            time.sleep(5)
            continue

    try:
        data = data['results']
    except Exception as e:
        print(e)
        return openapi_json, {}

    new_paths = {}
    for item in data:
        new_paths['/api/v1/exposed/{}/execute/'.format(item["id"])] = {
            'post': {  # assuming POST method for all operations
                'operationId': item['operation_id'],
                'description': item['description'],
                'parameters': [
                    {'name': k, 'in': 'query', 'required': True, 'schema': {'type': v}}
                    for k, v in item['params'].items()
                ],
                "security": {
                    "SessionAuth": [],
                    "AccessPointApiKeyHeader": [],
                    "AccessPointApiKeyQuery": [],
                    "AccessPointOAuth": []
                }
            }
        }

    openapi_json['paths'] = openapi_json['paths'] | new_paths

    return openapi_json, new_paths

# Reload the endpoints
def reload_endpoints(new_paths):
    new_endpoint2caller = {}
    for new_path in new_paths:
        # create the call function
        def call_api(input_json, api_key):
            import requests
            headers = {"X-API-Key": api_key}
            url = "https://nla.zapier.com" + new_path
            response = requests.post(url, headers=headers, json=input_json)

            if response.status_code == 200:
                return response.json()
            else:
                return response.text

        new_endpoint2caller[new_path] = call_api


                'description': item['description'],
                'parameters': [
                    {'name': k, 'in': 'query', 'required': True, 'schema': {'type': v}}
                    for k, v in item['params'].items()
                ],
                "security": {
                    "SessionAuth": [],
                    "AccessPointApiKeyHeader": [],
                    "AccessPointApiKeyQuery": [],
                    "AccessPointOAuth": []
                }
            }
        }

    openapi_json['paths'] = openapi_json['paths'] | new_paths

    return openapi_json, new_paths

# Reload the endpoints
def reload_endpoints(new_paths):
    new_endpoint2caller = {}
    for new_path in new_paths:
        # create the call function
        def call_api(input_json, api_key):
            import requests
            headers = {"X-API-Key": api_key}
            url = "https://nla.zapier.com" + new_path
            response = requests.post(url, headers=headers, json=input_json)

            if response.status_code == 200:
                return response.json()
            else:
                return response.text

        new_endpoint2caller[new_path] = call_api

    return new_endpoint2caller


                ],
                "security": {
                    "SessionAuth": [],
                    "AccessPointApiKeyHeader": [],
                    "AccessPointApiKeyQuery": [],
                    "AccessPointOAuth": []
                }
            }
        }

    openapi_json['paths'] = openapi_json['paths'] | new_paths

    return openapi_json, new_paths

# Reload the endpoints
def reload_endpoints(new_paths):
    new_endpoint2caller = {}
    for new_path in new_paths:
        # create the call function
        def call_api(input_json, api_key):
            import requests
            headers = {"X-API-Key": api_key}
            url = "https://nla.zapier.com" + new_path
            response = requests.post(url, headers=headers, json=input_json)

            if response.status_code == 200:
                return response.json()
            else:
                return response.text

        new_endpoint2caller[new_path] = call_api

    return new_endpoint2caller


"""Exposed paths for the Zapier plugin."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any], api_key) -> Dict[str, Any]:
    headers = {
        "X-API-Key": api_key,
    }
    url = "https://nla.zapier.com/api/v1/exposed/"
    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Configuration Link Path."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any], api_key) -> Dict[str, Any]:
    headers = {
        "X-API-Key": api_key,
    }
    url = "https://nla.zapier.com/api/v1/configuration-link/"
    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Preview a Zapier Zap."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any], api_key) -> Dict[str, Any]:
    headers = {
        "X-API-Key": api_key,
        "Content-Type": "application/json"
    }
    url = "https://nla.zapier.com/api/v1/preview-a-zap/"
    response = requests.post(url, headers=headers)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Execution log path for Zapier plugin."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any], api_key) -> Dict[str, Any]:
    headers = {
        "X-API-Key": api_key,
    }
    url = "https://nla.zapier.com/api/v1/" + input_json['execution-log']
    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Search Actions"""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any], api_key) -> Dict[str, Any]:
    headers = {
        "X-API-Key": api_key,
    }
    url = "https://nla.zapier.com/api/v1/search/actions/"
    response = requests.get(url, headers=headers, params=input_json)

    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Create QR Code API."""
from typing import Any, Dict

import requests

def call_api(input_json: Dict[str, Any]) -> Dict[str, Any]:
    url = "https://create-qr-code.modelxy.com/create-qr-code"
    response = requests.get(url, params=input_json)
    if response.status_code == 200:
        return response.json()
    else:
        return {"status_code": response.status_code, "text": response.text}


"""Executor that manage the plugins calling"""
from __future__ import annotations

from typing import Any, Callable, Dict, Union
from pydantic import BaseModel, Extra

from langchain.base_language import BaseLanguageModel

from real_agents.adapters.data_model import SpecModel
from real_agents.plugins_agent import APICallingChain
from real_agents.plugins_agent.plugins.utils import load_plugin_elements_by_name
from real_agents.adapters.data_model.utils import indent_multiline_string

class PluginExecutor(BaseModel):
    """Executor to call plugins that handle the spec showing, endpoint calling and output modeling."""
    name: str
    description: str
    spec_model: SpecModel
    meta_info: Dict[str, Any]
    endpoint2caller: Dict[str, Callable]
    endpoint2output_model: Dict[str, Callable]

    api_key: str = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def full_description(self, add_extra_description_from_plugin=True):
        description = (
            self.description + "\nOpenAPI information:\n" + indent_multiline_string(
                self.spec_model.prepare_spec())
            if add_extra_description_from_plugin
            else self.description
        )
        return description

    @classmethod
    def from_plugin_name(cls, plugin_name: str, ) -> PluginExecutor:
        plugin_info = load_plugin_elements_by_name(plugin_name)

        return cls(
            name=plugin_info["name"],
            description=plugin_info["description"],
            spec_model=plugin_info["spec_model"],
            meta_info=plugin_info["meta_info"],
            endpoint2caller=plugin_info["endpoint2caller"],
            endpoint2output_model=plugin_info["endpoint2output_model"],
        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}
        method = APICallingChain.from_llm_and_plugin(
            llm,
            self.meta_info,
            self.spec_model,
            self.endpoint2caller,
            self.endpoint2output_model,
            self.api_key,
        )

        output = method(inputs)
        return output

    def load_personnel_info(self):
        new_endpoint2caller, new_endpoints2output_model = self.spec_model.load_personnel_info(
            api_key=self.api_key)
        self.endpoint2caller = self.endpoint2caller | new_endpoint2caller
        self.endpoint2output_model = self.endpoint2output_model | new_endpoints2output_model


Config

from langchain.base_language import BaseLanguageModel

from real_agents.adapters.data_model import SpecModel
from real_agents.plugins_agent import APICallingChain
from real_agents.plugins_agent.plugins.utils import load_plugin_elements_by_name
from real_agents.adapters.data_model.utils import indent_multiline_string

class PluginExecutor(BaseModel):
    """Executor to call plugins that handle the spec showing, endpoint calling and output modeling."""
    name: str
    description: str
    spec_model: SpecModel
    meta_info: Dict[str, Any]
    endpoint2caller: Dict[str, Callable]
    endpoint2output_model: Dict[str, Callable]

    api_key: str = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def full_description(self, add_extra_description_from_plugin=True):
        description = (
            self.description + "\nOpenAPI information:\n" + indent_multiline_string(
                self.spec_model.prepare_spec())
            if add_extra_description_from_plugin
            else self.description
        )
        return description

    @classmethod
    def from_plugin_name(cls, plugin_name: str, ) -> PluginExecutor:
        plugin_info = load_plugin_elements_by_name(plugin_name)

        return cls(
            name=plugin_info["name"],
            description=plugin_info["description"],
            spec_model=plugin_info["spec_model"],
            meta_info=plugin_info["meta_info"],


class PluginExecutor(BaseModel):
    """Executor to call plugins that handle the spec showing, endpoint calling and output modeling."""
    name: str
    description: str
    spec_model: SpecModel
    meta_info: Dict[str, Any]
    endpoint2caller: Dict[str, Callable]
    endpoint2output_model: Dict[str, Callable]

    api_key: str = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def full_description(self, add_extra_description_from_plugin=True):
        description = (
            self.description + "\nOpenAPI information:\n" + indent_multiline_string(
                self.spec_model.prepare_spec())
            if add_extra_description_from_plugin
            else self.description
        )
        return description

    @classmethod
    def from_plugin_name(cls, plugin_name: str, ) -> PluginExecutor:
        plugin_info = load_plugin_elements_by_name(plugin_name)

        return cls(
            name=plugin_info["name"],
            description=plugin_info["description"],
            spec_model=plugin_info["spec_model"],
            meta_info=plugin_info["meta_info"],
            endpoint2caller=plugin_info["endpoint2caller"],
            endpoint2output_model=plugin_info["endpoint2output_model"],
        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}


    api_key: str = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def full_description(self, add_extra_description_from_plugin=True):
        description = (
            self.description + "\nOpenAPI information:\n" + indent_multiline_string(
                self.spec_model.prepare_spec())
            if add_extra_description_from_plugin
            else self.description
        )
        return description

    @classmethod
    def from_plugin_name(cls, plugin_name: str, ) -> PluginExecutor:
        plugin_info = load_plugin_elements_by_name(plugin_name)

        return cls(
            name=plugin_info["name"],
            description=plugin_info["description"],
            spec_model=plugin_info["spec_model"],
            meta_info=plugin_info["meta_info"],
            endpoint2caller=plugin_info["endpoint2caller"],
            endpoint2output_model=plugin_info["endpoint2output_model"],
        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}
        method = APICallingChain.from_llm_and_plugin(
            llm,
            self.meta_info,
            self.spec_model,
            self.endpoint2caller,
            self.endpoint2output_model,
            self.api_key,
        )

        output = method(inputs)
        return output

    def load_personnel_info(self):


run

            self.description + "\nOpenAPI information:\n" + indent_multiline_string(
                self.spec_model.prepare_spec())
            if add_extra_description_from_plugin
            else self.description
        )
        return description

    @classmethod
    def from_plugin_name(cls, plugin_name: str, ) -> PluginExecutor:
        plugin_info = load_plugin_elements_by_name(plugin_name)

        return cls(
            name=plugin_info["name"],
            description=plugin_info["description"],
            spec_model=plugin_info["spec_model"],
            meta_info=plugin_info["meta_info"],
            endpoint2caller=plugin_info["endpoint2caller"],
            endpoint2output_model=plugin_info["endpoint2output_model"],
        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}
        method = APICallingChain.from_llm_and_plugin(
            llm,
            self.meta_info,
            self.spec_model,
            self.endpoint2caller,
            self.endpoint2output_model,
            self.api_key,
        )

        output = method(inputs)
        return output

    def load_personnel_info(self):
        new_endpoint2caller, new_endpoints2output_model = self.spec_model.load_personnel_info(
            api_key=self.api_key)
        self.endpoint2caller = self.endpoint2caller | new_endpoint2caller
        self.endpoint2output_model = self.endpoint2output_model | new_endpoints2output_model


        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}
        method = APICallingChain.from_llm_and_plugin(
            llm,
            self.meta_info,
            self.spec_model,
            self.endpoint2caller,
            self.endpoint2output_model,
            self.api_key,
        )

        output = method(inputs)
        return output

    def load_personnel_info(self):
        new_endpoint2caller, new_endpoints2output_model = self.spec_model.load_personnel_info(
            api_key=self.api_key)
        self.endpoint2caller = self.endpoint2caller | new_endpoint2caller
        self.endpoint2output_model = self.endpoint2output_model | new_endpoints2output_model


"""Implement API calling."""

from __future__ import annotations

import re
import traceback
from typing import Any, Callable, Dict, List, Optional
import backoff
import json5
from fuzzywuzzy import process
from pydantic import BaseModel, Extra

from langchain.base_language import BaseLanguageModel
from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain.chains import LLMChain
from langchain.chains.base import Chain
from langchain.prompts.base import BasePromptTemplate
from langchain.prompts.chat import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate,
)

from real_agents.plugins_agent.api_calling.custom_exceptions import ParsingError, \
    APICallingError
from real_agents.adapters.data_model import SpecModel
from real_agents.adapters.memory import ReadOnlySharedStringMemory
from real_agents.plugins_agent.api_calling.prompt import (
    RETRY_PROMPT,
    STOP_PROMPT,
    SYSTEM_PROMPT,
    USER_PROMPT,
)

class APICallingChain(Chain, BaseModel):
    """Chain for Calling API"""

    llm_basic_chain: LLMChain
    llm_retry_chain: LLMChain
    llm_stop_chain: LLMChain

    meta_info: Dict[str, Any]
    spec_model: SpecModel
    endpoint2caller: Dict[str, Callable]
    endpoint2output_model: Dict[str, Callable]
    api_key: Optional[str] = None

    memory: Optional[ReadOnlySharedStringMemory] = None  # fixme:
    retry_times = 1
    stop: str = "\n\n"
    verbose = True

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["input_str"]

    @property
    def output_keys(self) -> List[str]:
        """Return the output keys.

        :meta private:
        """
        return ["endpoint", "input_json", "api_output"]

    @property
    def specs_str(self):
        """The str representation of spec."""
        return "\n".join(
            [
                f"{i}.\n{self.spec_model.prepare_spec_for_one_path(p, include_api_info=False)}"
                for i, p in enumerate(self.spec_model.paths.keys())
            ]
        )

    @property
    def need_auth(self):
        """Whether the API call needs authentication."""
        return not self.meta_info["manifest"]["auth"]["type"] in [
            None,
            "None",
            "none",
            "Null",
            "null",
        ]  # the value of type is not null in ai-plugin.json

    @backoff.on_exception(backoff.expo, Exception, max_tries=10, max_time=20)
    def call_api(self, endpoint, input_json):
        """Call the API and return the output. Wrap the data in the output model."""
        # Find the endpoint by fuzzy match, in case sometimes LLM generated a wrong endpoint
        if endpoint not in self.endpoint2caller:
            return "Endpoint not found. Please try again."

        endpoint = process.extractOne(endpoint, list(self.endpoint2caller.keys()))[0]

        # Add fuzzy match for endpoint
        try:
            api_output = (
                self.endpoint2caller[endpoint](input_json, self.api_key)
                if self.need_auth
                else self.endpoint2caller[endpoint](input_json)
            )
        except Exception as e:
            raise APICallingError(f"{e}")

        compressed_output = self.endpoint2output_model[endpoint]({"out": api_output})[
            "out"]
        return compressed_output

    def parse_response(self, response: str):
        """Parse the endpoint and input_json"""
        endpoint = None
        input_json = None

        try:
            json_content = json5.loads(response)
            endpoint = json_content["endpoint"]
            input_json = json_content["input_json"]
        except:
            pattern = r"```json\n(.+?)\n```" if "```json" in response else r"```\n(.+?)\n```"
            match = re.search(pattern, response, re.DOTALL)

            if match:
                try:
                    json_content = json5.loads(match.group(1))
                    endpoint = json_content["endpoint"]
                    input_json = json_content["input_json"]
                except Exception as e:
                    raise ParsingError(f"{e}")

        # When the endpoint is null, we use the default endpoint
        if endpoint is None or endpoint == "null" or endpoint == "Null" or endpoint == "NULL":
            endpoint = "\\"

        return {"endpoint": endpoint, "input_json": input_json}

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json
        input_variables = ["specs_str", "input_str"]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def create_retry_prompt(cls, system_prompt, retry_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json in retry
        input_variables = ["specs_str", "input_str", "trial_history"]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(retry_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def create_stop_prompt(cls, system_prompt, stop_prompt) -> BasePromptTemplate:
        # Decide the stop when the LLM are getting the predicted end_point and input_json
        input_variables = ["specs_str", "input_str", "api_output"]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(stop_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    def retry(self, input_str: str, trial_history: List[Dict],
              _run_manager: CallbackManagerForChainRun,
              vars_to_pass: Dict) -> None:
        response_content = (
            self.llm_basic_chain.run(
                **{"specs_str": self.specs_str, "input_str": input_str})
            if len(trial_history) == 0
            else self.llm_retry_chain.run(
                **{"specs_str": self.specs_str, "input_str": input_str,
                   "trial_history": trial_history}
            )
        )

        parsed_return = self.parse_response(response_content)

        _run_manager.on_text(parsed_return, indent=4, color="green",
                             verbose=self.verbose)

        endpoint, input_json = (
            parsed_return["endpoint"],
            parsed_return["input_json"],
        )

        vars_to_pass["endpoint"] = endpoint
        vars_to_pass["input_json"] = input_json

        api_output = self.call_api(endpoint, input_json)
        vars_to_pass["api_output"] = api_output

        _run_manager.on_text(api_output, color="yellow", verbose=self.verbose)

        should_stop = (
                self.llm_stop_chain.run(
                    **{"specs_str": self.specs_str, "input_str": input_str,
                       "api_output": api_output})
                .lower()
                .strip()
                == "yes"
        )
        _run_manager.on_text(should_stop, color="yellow", verbose=self.verbose)
        vars_to_pass["should_stop"] = should_stop

    def _call(
            self,
            inputs: Dict[str, str],
            run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, Any]:
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        input_str = inputs["input_str"]

        trial_history = []
        count = 0
        vars_to_pass = {"endpoint": None, "input_json": None, "api_output": None,
                        "should_stop": False}

        while count < self.retry_times:
            try:
                self.retry(input_str, trial_history, _run_manager, vars_to_pass)
                trial_history.append({"input_json": vars_to_pass["input_json"],
                                      "api_output": vars_to_pass["api_output"]})
                if vars_to_pass["should_stop"]:
                    break
                else:
                    count += 1
            except ParsingError as e:
                _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
                trial_history.append({"errors": str(e)})
                count += 1
                continue
            except APICallingError as e:
                _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
                trial_history.append({"errors": str(e)})
                count += 1
                continue
            except Exception as e:
                # fixme: Handle the exception, make error message shorter
                _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
                _run_manager.on_text(traceback.format_exc(), color="red",
                                     verbose=self.verbose)
                trial_history.append({"errors": str(e)})
                count += 1
                continue

        if count == self.retry_times:
            if "errors" in trial_history[-1]:
                return {"endpoint": vars_to_pass["endpoint"],
                        "input_json": vars_to_pass["input_json"],
                        "api_output": trial_history[-1]["errors"]}
            else:
                return {"endpoint": vars_to_pass["endpoint"]} | (
                    trial_history[-1])  # return the last trial history
        else:
            return {"endpoint": vars_to_pass["endpoint"],
                    "input_json": vars_to_pass["input_json"],
                    "api_output": vars_to_pass["api_output"]}

    @classmethod
    def from_llm_and_plugin(
            cls,
            llm: BaseLanguageModel,
            meta_info: Dict[str, Any],
            spec_model: SpecModel,
            endpoint2caller: Dict[str, Callable],
            endpoint2output_model: Dict[str, Callable],
            api_key: str,
    ) -> APICallingChain:
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_basic_prompt(
                system_prompt=SYSTEM_PROMPT,
                user_prompt=USER_PROMPT,
            ),
        )

        llm_retry_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=RETRY_PROMPT,
            ),
        )
        llm_stop_chain = LLMChain(
            llm=llm,
            prompt=cls.create_stop_prompt(
                system_prompt=SYSTEM_PROMPT,
                stop_prompt=STOP_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
            llm_retry_chain=llm_retry_chain,
            llm_stop_chain=llm_stop_chain,
            meta_info=meta_info,
            spec_model=spec_model,
            endpoint2caller=endpoint2caller,
            endpoint2output_model=endpoint2output_model,
            api_key=api_key,
        )