{ "cells": [ { "cell_type": "markdown", "id": "30995a82", "metadata": {}, "source": [ "# Conversational Agents using Tools\n", "\n", "This notebook takes you through how to use LangChain to augment an OpenAI model with access to external tools. In particular, you'll be able to create LLM agents that use custom tools to answer user queries.\n", "\n", "\n", "## What is Langchain\n", "\n", "LangChain is a [framework](https://python.langchain.com/en/latest/index.html) for developing applications powered by language models. Its main features include prompt chaining (running a sequence of language model queries in which outputs of earlier queries are made part of the input of later queries) and utilities for converting language models into tool-using agents.\n", "\n", "\n", "## Why do LLMs need to use Tools\n", "\n", "Providing LLMs access to tools can enable them to answer questions with context directly from search engines, APIs or your own databases. Instead of answering directly, an LLM with access to tools can perform intermediate steps to gather relevant information. Tools can also be used in combination. [For example](https://python.langchain.com/en/latest/modules/agents/agents/examples/mrkl_chat.html), a language model can be made to use a search tool to lookup quantitative information and a calculator to execute calculations.\n", "\n", "## Notebook Sections\n", "\n", "- **Setup:** Import packages, set any required variables and connect to a vector database.\n", "- **LLM Agent:** Build an agent that leverages a modified version of the [ReAct](https://react-lm.github.io/) framework to do chain-of-thought reasoning.\n", "- **LLM Agent with History:** Add the concept of memory so the LLM can draw on conversation context.\n", "- **Knowledge Base:** Create a knowledge base of Stuff You Should Know podcast episodes that we can use as a tool.\n", "- **LLM Agent with Tools:** Extend the agent with access to two tools and test that it uses both to answer questions." ] }, { "cell_type": "code", "execution_count": 2, "id": "9c069980", "metadata": {}, "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2" ] }, { "cell_type": "markdown", "id": "7a254003", "metadata": {}, "source": [ "# Setup\n", "\n", "Import libraries and set up a connection to a Pinecone vector database for our knowledge base.\n", "\n", "You can substitute Pinecone for any other vectorstore or database - there are a [selection](https://python.langchain.com/en/latest/modules/indexes/vectorstores.html) that are supported by Langchain natively, while other connectors will need to be developed yourself." ] }, { "cell_type": "code", "execution_count": null, "id": "f55905f7", "metadata": {}, "outputs": [], "source": [ "!pip install pinecone-client\n", "!pip install pandas\n", "!pip install typing\n", "!pip install tqdm\n", "!pip install langchain" ] }, { "cell_type": "code", "execution_count": 3, "id": "18be3d9f", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/colin.jarvis/Documents/dev/openai_scratchpad/prompt_chaining/pchain_env/lib/python3.10/site-packages/pinecone/index.py:4: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\n", " from tqdm.autonotebook import tqdm\n" ] } ], "source": [ "# Imports\n", "import os\n", "import pinecone\n", "import openai\n", "import pandas as pd\n", "import re\n", "from typing import List, Union\n", "import zipfile\n", "import json\n", "from tqdm.auto import tqdm\n", "import datetime\n", "\n", "\n", "# Langchain imports\n", "from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser\n", "from langchain.prompts import BaseChatPromptTemplate, ChatPromptTemplate\n", "from langchain import SerpAPIWrapper, LLMChain\n", "from langchain.schema import AgentAction, AgentFinish, HumanMessage, SystemMessage\n", "## LLM\n", "from langchain.chat_models import ChatOpenAI\n", "from langchain import OpenAI\n", "## Langchain memory\n", "from langchain.memory import ConversationBufferWindowMemory\n", "# Embeddings and vectorstore\n", "from langchain.embeddings.openai import OpenAIEmbeddings\n", "from langchain.vectorstores import Pinecone\n", "\n", "# Vectorstore Index\n", "index_name = 'podcasts'" ] }, { "cell_type": "code", "execution_count": 4, "id": "af825644", "metadata": {}, "outputs": [], "source": [ "api_key = os.getenv(\"PINECONE_API_KEY\")\n", "pinecone.init(api_key=api_key)" ] }, { "cell_type": "code", "execution_count": 5, "id": "c0518596", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['podcasts']" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pinecone.list_indexes()" ] }, { "cell_type": "markdown", "id": "7e7644cd", "metadata": {}, "source": [ "Run this code block if you want to clear the index, or if the index doesn't exist yet\n", "\n", "```\n", "# Check whether the index with the same name already exists - if so, delete it\n", "if index_name in pinecone.list_indexes():\n", " pinecone.delete_index(index_name)\n", " \n", "# Creates new index\n", "pinecone.create_index(name=index_name, dimension=1536)\n", "index = pinecone.Index(index_name=index_name)\n", "\n", "# Confirm our index was created\n", "pinecone.list_indexes()\n", "```" ] }, { "cell_type": "markdown", "id": "0eb63e37", "metadata": {}, "source": [ "## LLM Agent\n", "\n", "An [LLM agent](https://python.langchain.com/en/latest/modules/agents/agents/custom_llm_agent.html) in Langchain has many configurable components, which are detailed in the Langchain documentation.\n", "\n", "We'll employ a few of the core concepts to make an agent who talks in the way we want, can use tools to answer questions, and uses the right base LLM to power the conversation.\n", "- **Prompt Template:** The template to control the LLM's behaviour, in particular what inputs it expects and what outputs it produces ([docs](https://python.langchain.com/en/latest/modules/prompts/prompt_templates.html)).\n", "- **Output Parser:** The LLM can specify actions to be taken but these actions need to be parsed from its outputs ([docs](https://python.langchain.com/en/latest/modules/prompts/output_parsers.html)).\n", "- **LLM Chain:** A Chain brings together a prompt template with a LLM that will execute it - in this case we'll be using ```gpt-3.5-turbo```([docs](https://python.langchain.com/en/latest/modules/chains.html)).\n", "- **Tool:** An external service that the LLM can use to retrieve information or execute commands should the user require it ([docs](https://python.langchain.com/en/latest/modules/agents/tools.html)).\n", "- **Agent:** The glue that brings all of this together, an agent can call multiple LLM Chains, each with their own tools. Agents can be extended with your own logic to allow retries, error handling and any other methods you choose to add reliability to your application ([docs](https://python.langchain.com/en/latest/modules/agents.html)).\n", "\n", "**NB:** Before using this cookbook with the Search tool you'll need to sign up on https://serpapi.com/ and generate an API key. Once you have it, store it in an environment variable named ```SERPAPI_API_KEY```" ] }, { "cell_type": "code", "execution_count": 6, "id": "3f813668", "metadata": {}, "outputs": [], "source": [ "# Initiate a Search tool - note you'll need to have set SERPAPI_API_KEY as an environment variable as per the above instructions\n", "search = SerpAPIWrapper()\n", "\n", "# Define a list of tools\n", "tools = [\n", " Tool(\n", " name = \"Search\",\n", " func=search.run,\n", " description=\"useful for when you need to answer questions about current events\"\n", " )\n", "]" ] }, { "cell_type": "code", "execution_count": 7, "id": "effae642", "metadata": {}, "outputs": [], "source": [ "# Set up the prompt with input variables for tools, user input and a scratchpad for the model to record its workings\n", "template = \"\"\"Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:\n", "\n", "{tools}\n", "\n", "Use the following format:\n", "\n", "Question: the input question you must answer\n", "Thought: you should always think about what to do\n", "Action: the action to take, should be one of [{tool_names}]\n", "Action Input: the input to the action\n", "Observation: the result of the action\n", "... (this Thought/Action/Action Input/Observation can repeat N times)\n", "Thought: I now know the final answer\n", "Final Answer: the final answer to the original input question\n", "\n", "Begin! Remember to speak as a pirate when giving your final answer. Use lots of \"Arg\"s\n", "\n", "Question: {input}\n", "{agent_scratchpad}\"\"\"" ] }, { "cell_type": "code", "execution_count": 8, "id": "b4efd85d", "metadata": {}, "outputs": [], "source": [ "# Set up a prompt template\n", "class CustomPromptTemplate(BaseChatPromptTemplate):\n", " # The template to use\n", " template: str\n", " # The list of tools available\n", " tools: List[Tool]\n", " \n", " def format_messages(self, **kwargs) -> str:\n", " # Get the intermediate steps (AgentAction, Observation tuples)\n", " \n", " # Format them in a particular way\n", " intermediate_steps = kwargs.pop(\"intermediate_steps\")\n", " thoughts = \"\"\n", " for action, observation in intermediate_steps:\n", " thoughts += action.log\n", " thoughts += f\"\\nObservation: {observation}\\nThought: \"\n", " \n", " # Set the agent_scratchpad variable to that value\n", " kwargs[\"agent_scratchpad\"] = thoughts\n", " \n", " # Create a tools variable from the list of tools provided\n", " kwargs[\"tools\"] = \"\\n\".join([f\"{tool.name}: {tool.description}\" for tool in self.tools])\n", " \n", " # Create a list of tool names for the tools provided\n", " kwargs[\"tool_names\"] = \", \".join([tool.name for tool in self.tools])\n", " formatted = self.template.format(**kwargs)\n", " return [HumanMessage(content=formatted)]\n", " \n", "prompt = CustomPromptTemplate(\n", " template=template,\n", " tools=tools,\n", " # This omits the `agent_scratchpad`, `tools`, and `tool_names` variables because those are generated dynamically\n", " # This includes the `intermediate_steps` variable because that is needed\n", " input_variables=[\"input\", \"intermediate_steps\"]\n", ")" ] }, { "cell_type": "code", "execution_count": 9, "id": "533657a5", "metadata": {}, "outputs": [], "source": [ "class CustomOutputParser(AgentOutputParser):\n", " \n", " def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:\n", " \n", " # Check if agent should finish\n", " if \"Final Answer:\" in llm_output:\n", " return AgentFinish(\n", " # Return values is generally always a dictionary with a single `output` key\n", " # It is not recommended to try anything else at the moment :)\n", " return_values={\"output\": llm_output.split(\"Final Answer:\")[-1].strip()},\n", " log=llm_output,\n", " )\n", " \n", " # Parse out the action and action input\n", " regex = r\"Action: (.*?)[\\n]*Action Input:[\\s]*(.*)\"\n", " match = re.search(regex, llm_output, re.DOTALL)\n", " \n", " # If it can't parse the output it raises an error\n", " # You can add your own logic here to handle errors in a different way i.e. pass to a human, give a canned response\n", " if not match:\n", " raise ValueError(f\"Could not parse LLM output: `{llm_output}`\")\n", " action = match.group(1).strip()\n", " action_input = match.group(2)\n", " \n", " # Return the action and action input\n", " return AgentAction(tool=action, tool_input=action_input.strip(\" \").strip('\"'), log=llm_output)\n", " \n", "output_parser = CustomOutputParser()" ] }, { "cell_type": "code", "execution_count": 10, "id": "4c7d93d1", "metadata": {}, "outputs": [], "source": [ "# Initiate our LLM - default is 'gpt-3.5-turbo'\n", "llm = ChatOpenAI(temperature=0)\n", "\n", "# LLM chain consisting of the LLM and a prompt\n", "llm_chain = LLMChain(llm=llm, prompt=prompt)\n", "\n", "# Using tools, the LLM chain and output_parser to make an agent\n", "tool_names = [tool.name for tool in tools]\n", "\n", "agent = LLMSingleActionAgent(\n", " llm_chain=llm_chain, \n", " output_parser=output_parser,\n", " # We use \"Observation\" as our stop sequence so it will stop when it receives Tool output\n", " # If you change your prompt template you'll need to adjust this as well\n", " stop=[\"\\nObservation:\"], \n", " allowed_tools=tool_names\n", ")" ] }, { "cell_type": "code", "execution_count": 11, "id": "dd893fa5", "metadata": {}, "outputs": [], "source": [ "# Initiate the agent that will respond to our queries\n", "# Set verbose=True to share the CoT reasoning the LLM goes through\n", "agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True)" ] }, { "cell_type": "code", "execution_count": 12, "id": "c7ca3ee2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n", "\u001B[32;1m\u001B[1;3mThought: Hmm, I be not sure of the answer to that one. Let me think.\n", "Action: Search\n", "Action Input: \"Canada population 2023\"\u001B[0m\n", "\n", "Observation:\u001B[36;1m\u001B[1;3mThe current population of Canada is 38,662,830 as of Monday, April 17, 2023, based on Worldometer elaboration of the latest United Nations data.\u001B[0m\u001B[32;1m\u001B[1;3mAhoy, that be the answer I was lookin' for!\n", "Final Answer: The population of Canada as of 2023 be 38,662,830, Arg!\u001B[0m\n", "\n", "\u001B[1m> Finished chain.\u001B[0m\n" ] }, { "data": { "text/plain": [ "'The population of Canada as of 2023 be 38,662,830, Arg!'" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "agent_executor.run(\"How many people live in canada as of 2023?\")" ] }, { "cell_type": "markdown", "id": "c3b51817", "metadata": {}, "source": [ "## LLM Agent with History\n", "\n", "Extend the LLM Agent with the ability to retain a [memory](https://python.langchain.com/en/latest/modules/agents/agents/custom_llm_agent.html#adding-memory) and use it as context as it continues the conversation.\n", "\n", "We use a simple ConversationBufferWindowMemory for this example that keeps a rolling window of the last two conversation turns, but LangChain have many other [memory options](https://python.langchain.com/en/latest/modules/memory.html) depending on your use case." ] }, { "cell_type": "code", "execution_count": 13, "id": "85737eea", "metadata": {}, "outputs": [], "source": [ "# Set up a prompt template which can interpolate the history\n", "template_with_history = \"\"\"You are SearchGPT, a professional search engine who aims to provide informative answers to users. Answer the following questions as best you can. You have access to the following tools:\n", "\n", "{tools}\n", "\n", "Use the following format:\n", "\n", "Question: the input question you must answer\n", "Thought: you should always think about what to do\n", "Action: the action to take, should be one of [{tool_names}]\n", "Action Input: the input to the action\n", "Observation: the result of the action\n", "... (this Thought/Action/Action Input/Observation can repeat N times)\n", "Thought: I now know the final answer\n", "Final Answer: the final answer to the original input question\n", "\n", "Begin! Remember to give detailed, informative answers\n", "\n", "Previous conversation history:\n", "{history}\n", "\n", "New question: {input}\n", "{agent_scratchpad}\"\"\"" ] }, { "cell_type": "code", "execution_count": 14, "id": "4d85c309", "metadata": {}, "outputs": [], "source": [ "prompt_with_history = CustomPromptTemplate(\n", " template=template_with_history,\n", " tools=tools,\n", " # The history template includes \"history\" as an input variable so we can interpolate it into the prompt\n", " input_variables=[\"input\", \"intermediate_steps\", \"history\"]\n", ")\n", "\n", "llm_chain = LLMChain(llm=llm, prompt=prompt_with_history)\n", "tool_names = [tool.name for tool in tools]\n", "agent = LLMSingleActionAgent(\n", " llm_chain=llm_chain, \n", " output_parser=output_parser,\n", " stop=[\"\\nObservation:\"], \n", " allowed_tools=tool_names\n", ")" ] }, { "cell_type": "code", "execution_count": 15, "id": "4f8c8fea", "metadata": {}, "outputs": [], "source": [ "# Initiate the memory with k=2 to keep the last two turns\n", "# Provide the memory to the agent\n", "memory = ConversationBufferWindowMemory(k=2)\n", "agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True, memory=memory)" ] }, { "cell_type": "code", "execution_count": 16, "id": "e1f031bb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n", "\u001B[32;1m\u001B[1;3mThought: I need to find the most recent population data for Canada.\n", "Action: Search\n", "Action Input: \"Canada population 2023\"\u001B[0m\n", "\n", "Observation:\u001B[36;1m\u001B[1;3mThe current population of Canada is 38,662,830 as of Monday, April 17, 2023, based on Worldometer elaboration of the latest United Nations data.\u001B[0m\u001B[32;1m\u001B[1;3mThat's the answer to the question.\n", "Final Answer: As of April 17, 2023, the population of Canada is 38,662,830.\u001B[0m\n", "\n", "\u001B[1m> Finished chain.\u001B[0m\n" ] }, { "data": { "text/plain": [ "'As of April 17, 2023, the population of Canada is 38,662,830.'" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "agent_executor.run(\"How many people live in canada as of 2023?\")" ] }, { "cell_type": "code", "execution_count": 17, "id": "9d28d73f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n", "\u001B[32;1m\u001B[1;3mThought: I need to search for the current population of Mexico.\n", "Action: Search\n", "Action Input: \"current population of Mexico\"\u001B[0m\n", "\n", "Observation:\u001B[36;1m\u001B[1;3mMexico, officially the United Mexican States, is a country in the southern portion of North America. It is bordered to the north by the United States; to the south and west by the Pacific Ocean; to the southeast by Guatemala, Belize, and the Caribbean Sea; and to the east by the Gulf of Mexico.\u001B[0m\u001B[32;1m\u001B[1;3mThat's not the answer to the question, I need to refine my search.\n", "Action: Search\n", "Action Input: \"population of Mexico 2023\"\u001B[0m\n", "\n", "Observation:\u001B[36;1m\u001B[1;3m128,455,567\u001B[0m\u001B[32;1m\u001B[1;3mI now know the final answer.\n", "Final Answer: As of 2023, the population of Mexico is 128,455,567.\u001B[0m\n", "\n", "\u001B[1m> Finished chain.\u001B[0m\n" ] }, { "data": { "text/plain": [ "'As of 2023, the population of Mexico is 128,455,567.'" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "agent_executor.run(\"how about in mexico?\")" ] }, { "cell_type": "markdown", "id": "62356eed", "metadata": {}, "source": [ "## Knowledge base\n", "\n", "Create a custom vectorstore for the Agent to use as a Tool to answer questions with. We'll store the results in [Pinecone](https://docs.pinecone.io/docs/quickstart), which is supported by LangChain ([Docs](https://python.langchain.com/en/latest/modules/indexes/vectorstores/examples/pinecone.html), [API reference](https://python.langchain.com/en/latest/reference/modules/vectorstore.html)). For help getting started with Pinecone or other vector databases, we have a [cookbook](https://github.com/openai/openai-cookbook/blob/colin/examples/vector_databases/Using_vector_databases_for_embeddings_search.ipynb) to help you get started.\n", "\n", "You can check the LangChain documentation to see what other [vectorstores](https://python.langchain.com/en/latest/modules/indexes/vectorstores.html) and [databases](https://python.langchain.com/en/latest/modules/chains/examples/sqlite.html) are available.\n", "\n", "For this example we'll use the transcripts of the Stuff You Should Know podcast, which was provided thanks to OSF DOI [10.17605/OSF.IO/VM9NT](https://doi.org/10.17605/OSF.IO/VM9NT)" ] }, { "cell_type": "code", "execution_count": 18, "id": "87f86008", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "100% [......................................................................] 571275039 / 571275039" ] }, { "data": { "text/plain": [ "'sysk_podcast_transcripts_embedded.json.zip'" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import wget\n", "\n", "# Here is a URL to a zip archive containing the transcribed podcasts\n", "# Note that this data has already been split into chunks and embeddings from OpenAI's text-embedding-ada-002 embedding model are included\n", "content_url = 'https://cdn.openai.com/API/examples/data/sysk_podcast_transcripts_embedded.json.zip'\n", "\n", "# Download the file (it is ~541 MB so this will take some time)\n", "wget.download(content_url)" ] }, { "cell_type": "code", "execution_count": 19, "id": "4889c292", "metadata": {}, "outputs": [], "source": [ "# Load podcasts\n", "with zipfile.ZipFile(\"sysk_podcast_transcripts_embedded.json.zip\",\"r\") as zip_ref:\n", " zip_ref.extractall(\"./data\")\n", "f = open('./data/sysk_podcast_transcripts_embedded.json')\n", "processed_podcasts = json.load(f)" ] }, { "cell_type": "code", "execution_count": 20, "id": "1070ef60", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " | id | \n", "filename | \n", "title | \n", "url | \n", "text_chunk | \n", "embedding | \n", "cleaned_id | \n", "
---|---|---|---|---|---|---|---|
0 | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "\\n\\nSYSK Selects How Crime Scene Cleanup Works | \n", "https://chtbl.com/track/5899E/podtrac.com/pts/... | \n", "Title: sysk_with_transcripts_SYSK Selects How ... | \n", "[0.021279960870742798, -0.005817972123622894, ... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "
1 | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "\\n\\nSYSK Selects How Crime Scene Cleanup Works | \n", "https://chtbl.com/track/5899E/podtrac.com/pts/... | \n", "Title: sysk_with_transcripts_SYSK Selects How ... | \n", "[0.013859338127076626, 0.00857278611510992, 0.... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "
2 | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "\\n\\nSYSK Selects How Crime Scene Cleanup Works | \n", "https://chtbl.com/track/5899E/podtrac.com/pts/... | \n", "Title: sysk_with_transcripts_SYSK Selects How ... | \n", "[0.015242221765220165, 0.016030369326472282, 0... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "
3 | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "\\n\\nSYSK Selects How Crime Scene Cleanup Works | \n", "https://chtbl.com/track/5899E/podtrac.com/pts/... | \n", "Title: sysk_with_transcripts_SYSK Selects How ... | \n", "[0.004371842369437218, -0.003036574460566044, ... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "
4 | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "\\n\\nSYSK Selects How Crime Scene Cleanup Works | \n", "https://chtbl.com/track/5899E/podtrac.com/pts/... | \n", "Title: sysk_with_transcripts_SYSK Selects How ... | \n", "[0.017309172078967094, 0.015154214575886726, 0... | \n", "sysk_with_transcripts_SYSK Selects How Crime S... | \n", "