diff --git a/examples/managing-conversation-history.ipynb b/examples/managing-conversation-history.ipynb new file mode 100644 index 000000000..65719acac --- /dev/null +++ b/examples/managing-conversation-history.ipynb @@ -0,0 +1,367 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# How to manage conversation history\n", + "\n", + "One of the most common use cases for persistence is to use it to keep track of conversation history. This is great - it makes it easy to continue conversations. As conversations get longer and longer, however, this conversation history can build up and take up more and more of the context window. This can often be undesirable as it leads to more expensive and longer calls to the LLM, and potentially ones that error. In this notebook we will discuss a few strategies for how to deal with this." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First, let's set up the packages we're going to want to use" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_anthropic" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for Anthropic (the LLM we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"ANTHROPIC_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "4767ef1c-a7cf-41f8-a301-558988cb7ac5", + "metadata": {}, + "source": [ + "Let's now build a simple ReAct style agent." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "378899a9-3b9a-4748-95b6-eb00e0828677", + "metadata": {}, + "outputs": [], + "source": [ + "from typing_extensions import TypedDict\n", + "from typing import Annotated\n", + "from langgraph.graph import MessagesState\n", + "from langchain_core.tools import tool\n", + "from langgraph.prebuilt import ToolNode\n", + "from langchain_anthropic import ChatAnthropic\n", + "from typing import Literal\n", + "from langgraph.graph import StateGraph, END\n", + "from langgraph.checkpoint.sqlite import SqliteSaver\n", + "\n", + "memory = SqliteSaver.from_conn_string(\":memory:\")\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " # Don't let the LLM know this though 😊\n", + " return [\n", + " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", + " ]\n", + "\n", + "\n", + "tools = [search]\n", + "tool_node = ToolNode(tools)\n", + "model = ChatAnthropic(model_name=\"claude-3-haiku-20240307\")\n", + "bound_model = model.bind_tools(tools)\n", + "\n", + "def should_continue(state: MessagesState) -> Literal[\"action\", \"__end__\"]:\n", + " \"\"\"Return the next node to execute.\"\"\"\n", + " last_message = state[\"messages\"][-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"__end__\"\n", + " # Otherwise if there is, we continue\n", + " return \"action\"\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state: MessagesState):\n", + " response = model.invoke(state[\"messages\"])\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": response}\n", + "\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(MessagesState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", tool_node)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "57b27553-21be-43e5-ac48-d1d0a3aa0dca", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "hi! I'm bob\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Nice to meet you, Bob! As an AI assistant, I don't have a physical form, but I'm happy to chat with you and try my best to help out however I can. Please feel free to ask me anything, and I'll do my best to provide useful information or assistance.\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "whats my name?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "You said your name is Bob, so that is the name I have for you.\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "input_message = HumanMessage(content=\"hi! I'm bob\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()\n", + "\n", + "\n", + "input_message = HumanMessage(content=\"whats my name?\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "5d5da4c9-ba8b-46cb-a860-63fe585d15c5", + "metadata": {}, + "source": [ + "## Filtering messages\n", + "\n", + "The most straight-forward thing to do to prevent conversation history from blowing up is to filter the list of messages before they get passed to the LLM. This involves two parts: defining a function to filter messages, and then adding it to the graph. See the example below which defines a really simple `filter_messages` function and then uses it." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "eb20430f", + "metadata": {}, + "outputs": [], + "source": [ + "from typing_extensions import TypedDict\n", + "from typing import Annotated\n", + "from langgraph.graph import MessagesState\n", + "from langchain_core.tools import tool\n", + "from langgraph.prebuilt import ToolNode\n", + "from langchain_anthropic import ChatAnthropic\n", + "from typing import Literal\n", + "from langgraph.graph import StateGraph, END\n", + "from langgraph.checkpoint.sqlite import SqliteSaver\n", + "\n", + "memory = SqliteSaver.from_conn_string(\":memory:\")\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " # Don't let the LLM know this though 😊\n", + " return [\n", + " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", + " ]\n", + "\n", + "\n", + "tools = [search]\n", + "tool_node = ToolNode(tools)\n", + "model = ChatAnthropic(model_name=\"claude-3-haiku-20240307\")\n", + "bound_model = model.bind_tools(tools)\n", + "\n", + "def should_continue(state: MessagesState) -> Literal[\"action\", \"__end__\"]:\n", + " \"\"\"Return the next node to execute.\"\"\"\n", + " last_message = state[\"messages\"][-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"__end__\"\n", + " # Otherwise if there is, we continue\n", + " return \"action\"\n", + "\n", + "\n", + "def filter_messages(messages: list):\n", + " # This is very simple helper function which only ever uses the last two messages\n", + " return messages[-1:]\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state: MessagesState):\n", + " messages = filter_messages(state[\"messages\"])\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": response}\n", + "\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(MessagesState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", tool_node)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "52468ebb-4b23-45ac-a98e-b4439f37740a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "hi! I'm bob\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Nice to meet you, Bob! I'm Claude, an AI assistant created by Anthropic. It's a pleasure to chat with you. Feel free to ask me anything, I'm here to help!\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "whats my name?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "I'm afraid I don't actually know your name. As an AI assistant, I don't have information about the specific identities of the people I talk to. I only know what is provided to me during our conversation.\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "input_message = HumanMessage(content=\"hi! I'm bob\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()\n", + "\n", + "# This will now not remember the previous messages \n", + "# (because we set `messages[-1:]` in the filter messages argument)\n", + "input_message = HumanMessage(content=\"whats my name?\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}