# -*- coding: utf-8 -*-
import os
import re
import sys
sys.path.append('.')
sys.path.append('..')
from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser
from langchain.prompts import StringPromptTemplate
from langchain import OpenAI, GoogleSearchAPIWrapper, LLMChain
from typing import List, Union, Callable
from langchain.schema import AgentAction, AgentFinish
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
from langchain.schema import Document
from utils.configs import configs

os.environ["GOOGLE_CSE_ID"] = configs['tools']['google_cse_id']
os.environ["GOOGLE_API_KEY"] = configs['tools']['google_api_key']
os.environ["OPENAI_API_KEY"] = configs['openai_api_key']


# Set up the base template
template = """Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Arg"s

Question: {input}
{agent_scratchpad}"""


def fake_func(inp: str) -> str:
    return "foo"


def get_tools(query):
    docs = retriever.get_relevant_documents(query)
    return [ALL_TOOLS[d.metadata["index"]] for d in docs]


# Set up a prompt template
class CustomPromptTemplate(StringPromptTemplate):
    # The template to use
    template: str
    ############## NEW ######################
    # The list of tools available
    tools_getter: Callable

    def format(self, **kwargs) -> str:
        # Get the intermediate steps (AgentAction, Observation tuples)
        # Format them in a particular way
        intermediate_steps = kwargs.pop("intermediate_steps")
        thoughts = ""
        for action, observation in intermediate_steps:
            thoughts += action.log
            thoughts += f"\nObservation: {observation}\nThought: "
        # Set the agent_scratchpad variable to that value
        kwargs["agent_scratchpad"] = thoughts
        ############## NEW ######################
        tools = self.tools_getter(kwargs["input"])
        # Create a tools variable from the list of tools provided
        kwargs["tools"] = "\n".join([f"{tool.name}: {tool.description}" for tool in tools])
        # Create a list of tool names for the tools provided
        kwargs["tool_names"] = ", ".join([tool.name for tool in tools])
        print(self.template.format(**kwargs))
        return self.template.format(**kwargs)


class CustomOutputParser(AgentOutputParser):

    def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
        # Check if agent should finish
        if "Final Answer:" in llm_output:
            return AgentFinish(
                # Return values is generally always a dictionary with a single `output` key
                # It is not recommended to try anything else at the moment :)
                return_values={"output": llm_output.split("Final Answer:")[-1].strip()},
                log=llm_output,
            )
        # Parse out the action and action input
        regex = r"Action: (.*?)[\n]*Action Input:[\s]*(.*)"
        match = re.search(regex, llm_output, re.DOTALL)
        if not match:
            raise ValueError(f"Could not parse LLM output: `{llm_output}`")
        action = match.group(1).strip()
        action_input = match.group(2)
        # Return the action and action input
        return AgentAction(tool=action, tool_input=action_input.strip(" ").strip('"'), log=llm_output)


if __name__ == '__main__':
    # Define which tools the agent can use to answer user queries
    search = GoogleSearchAPIWrapper()

    search_tool = Tool(
        name="Search",
        func=search.run,
        description="useful for when you need to answer questions about current events"
    )
    fake_tools = [
        Tool(
            name=f"foo-{i}",
            func=fake_func,
            description=f"a silly function that you can use to get more information about the number {i}"
        )
        for i in range(99)
    ]

    ALL_TOOLS = [search_tool] + fake_tools

    # tools retrieval
    tool_lib = configs['demo_agents']['tool_faiss_index']
    if os.path.exists(tool_lib):
        vector_store = FAISS.load_local(tool_lib, OpenAIEmbeddings())
    else:
        docs = [Document(page_content=t.description, metadata={"index": i}) for i, t in enumerate(ALL_TOOLS)]
        vector_store = FAISS.from_documents(docs, OpenAIEmbeddings())
        vector_store.save_local(tool_lib)
    retriever = vector_store.as_retriever()

    prompt = CustomPromptTemplate(
        template=template,
        tools_getter=get_tools,
        # This omits the `agent_scratchpad`, `tools`, and `tool_names` variables because those are generated dynamically
        # This includes the `intermediate_steps` variable because that is needed
        input_variables=["input", "intermediate_steps"]
    )

    output_parser = CustomOutputParser()

    model_name = configs['model_name']
    llm = OpenAI(model_name=model_name, temperature=0)
    # LLM chain consisting of the LLM and a prompt
    llm_chain = LLMChain(llm=llm, prompt=prompt)

    query = "What's the weather in SF?"
    tools = get_tools(query)
    tool_names = [tool.name for tool in tools]
    agent = LLMSingleActionAgent(
        llm_chain=llm_chain,
        output_parser=output_parser,
        stop=["\nObservation:"],
        allowed_tools=tool_names
    )

    agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True)

    agent_executor.run(query)


langchain.agents.AgentExecutor.from_agent_and_tools langchain.agents.LLMSingleActionAgent langchain.LLMChain langchain.GoogleSearchAPIWrapper langchain.schema.Document langchain.agents.Tool langchain.embeddings.OpenAIEmbeddings langchain.OpenAI
[((55, 75), 'sys.path.append', 'sys.path.append', (['"""."""'], {}), "('.')\n", (70, 75), False, 'import sys\n'), ((76, 97), 'sys.path.append', 'sys.path.append', (['""".."""'], {}), "('..')\n", (91, 97), False, 'import sys\n'), ((4014, 4038), 'langchain.GoogleSearchAPIWrapper', 'GoogleSearchAPIWrapper', ([], {}), '()\n', (4036, 4038), False, 'from langchain import OpenAI, GoogleSearchAPIWrapper, LLMChain\n'), ((4058, 4180), 'langchain.agents.Tool', 'Tool', ([], {'name': '"""Search"""', 'func': 'search.run', 'description': '"""useful for when you need to answer questions about current events"""'}), "(name='Search', func=search.run, description=\n    'useful for when you need to answer questions about current events')\n", (4062, 4180), False, 'from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser\n'), ((4577, 4601), 'os.path.exists', 'os.path.exists', (['tool_lib'], {}), '(tool_lib)\n', (4591, 4601), False, 'import os\n'), ((5400, 5444), 'langchain.OpenAI', 'OpenAI', ([], {'model_name': 'model_name', 'temperature': '(0)'}), '(model_name=model_name, temperature=0)\n', (5406, 5444), False, 'from langchain import OpenAI, GoogleSearchAPIWrapper, LLMChain\n'), ((5512, 5544), 'langchain.LLMChain', 'LLMChain', ([], {'llm': 'llm', 'prompt': 'prompt'}), '(llm=llm, prompt=prompt)\n', (5520, 5544), False, 'from langchain import OpenAI, GoogleSearchAPIWrapper, LLMChain\n'), ((5674, 5800), 'langchain.agents.LLMSingleActionAgent', 'LLMSingleActionAgent', ([], {'llm_chain': 'llm_chain', 'output_parser': 'output_parser', 'stop': "['\\nObservation:']", 'allowed_tools': 'tool_names'}), "(llm_chain=llm_chain, output_parser=output_parser, stop\n    =['\\nObservation:'], allowed_tools=tool_names)\n", (5694, 5800), False, 'from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser\n'), ((5856, 5930), 'langchain.agents.AgentExecutor.from_agent_and_tools', 'AgentExecutor.from_agent_and_tools', ([], {'agent': 'agent', 'tools': 'tools', 'verbose': '(True)'}), '(agent=agent, tools=tools, verbose=True)\n', (5890, 5930), False, 'from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser\n'), ((3542, 3581), 're.search', 're.search', (['regex', 'llm_output', 're.DOTALL'], {}), '(regex, llm_output, re.DOTALL)\n', (3551, 3581), False, 'import re\n'), ((4233, 4375), 'langchain.agents.Tool', 'Tool', ([], {'name': 'f"""foo-{i}"""', 'func': 'fake_func', 'description': 'f"""a silly function that you can use to get more information about the number {i}"""'}), "(name=f'foo-{i}', func=fake_func, description=\n    f'a silly function that you can use to get more information about the number {i}'\n    )\n", (4237, 4375), False, 'from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser\n'), ((4653, 4671), 'langchain.embeddings.OpenAIEmbeddings', 'OpenAIEmbeddings', ([], {}), '()\n', (4669, 4671), False, 'from langchain.embeddings import OpenAIEmbeddings\n'), ((4699, 4758), 'langchain.schema.Document', 'Document', ([], {'page_content': 't.description', 'metadata': "{'index': i}"}), "(page_content=t.description, metadata={'index': i})\n", (4707, 4758), False, 'from langchain.schema import Document\n'), ((4843, 4861), 'langchain.embeddings.OpenAIEmbeddings', 'OpenAIEmbeddings', ([], {}), '()\n', (4859, 4861), False, 'from langchain.embeddings import OpenAIEmbeddings\n')]