diff --git a/docs/docs/tutorials/chatbot.ipynb b/docs/docs/tutorials/chatbot.ipynb index c05db80cb74..6ded546a2c5 100644 --- a/docs/docs/tutorials/chatbot.ipynb +++ b/docs/docs/tutorials/chatbot.ipynb @@ -35,6 +35,14 @@ "\n", ":::\n", "\n", + ":::{.callout-note}\n", + "\n", + "This tutorial previously built a chatbot using [RunnableWithMessageHistory](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.history.RunnableWithMessageHistory.html). You can access this version of the tutorial in the [v0.2 docs](https://python.langchain.com/v0.2/docs/tutorials/chatbot/).\n", + "\n", + "The LangGraph implementation offers a number of advantages over `RunnableWithMessageHistory`, including the ability to persist arbitrary components of an application's state (instead of only messages).\n", + "\n", + ":::\n", + "\n", "## Overview\n", "\n", "We'll go over an example of how to design and implement an LLM-powered chatbot. \n", @@ -59,7 +67,7 @@ "\n", "### Installation\n", "\n", - "To install LangChain run:\n", + "For this tutorial we will need `langchain-core` and `langgraph`:\n", "\n", "```{=mdx}\n", "import Tabs from '@theme/Tabs';\n", @@ -68,10 +76,10 @@ "\n", "\n", " \n", - " pip install langchain\n", + " pip install langchain-core langgraph\n", " \n", " \n", - " conda install langchain -c conda-forge\n", + " conda install langchain-core langgraph -c conda-forge\n", " \n", "\n", "\n", @@ -110,13 +118,13 @@ "```{=mdx}\n", "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", "\n", - "\n", + "\n", "```" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -125,7 +133,7 @@ "\n", "from langchain_openai import ChatOpenAI\n", "\n", - "model = ChatOpenAI(model=\"gpt-3.5-turbo\")" + "model = ChatOpenAI(model=\"gpt-4o-mini\")" ] }, { @@ -137,16 +145,16 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content='Hello Bob! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 12, 'total_tokens': 22}, 'model_name': 'gpt-4o-mini', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-d939617f-0c3b-45e9-a93f-13dafecbd4b5-0', usage_metadata={'input_tokens': 12, 'output_tokens': 10, 'total_tokens': 22})" + "AIMessage(content='Hi Bob! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 11, 'total_tokens': 21, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_1bb46167f9', 'finish_reason': 'stop', 'logprobs': None}, id='run-149994c0-d958-49bb-9a9d-df911baea29f-0', usage_metadata={'input_tokens': 11, 'output_tokens': 10, 'total_tokens': 21})" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -166,16 +174,16 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content=\"I'm sorry, I don't have access to personal information unless you provide it to me. How may I assist you today?\", response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 12, 'total_tokens': 38}, 'model_name': 'gpt-4o-mini', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-47bc8c20-af7b-4fd2-9345-f0e9fdf18ce3-0', usage_metadata={'input_tokens': 12, 'output_tokens': 26, 'total_tokens': 38})" + "AIMessage(content=\"I'm sorry, but I don't have access to personal information about individuals unless you've shared it with me in this conversation. How can I assist you today?\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 30, 'prompt_tokens': 11, 'total_tokens': 41, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_1bb46167f9', 'finish_reason': 'stop', 'logprobs': None}, id='run-0ecab57c-728d-4fd1-845c-394a62df8e13-0', usage_metadata={'input_tokens': 11, 'output_tokens': 30, 'total_tokens': 41})" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -198,16 +206,16 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content='Your name is Bob. How can I help you, Bob?', response_metadata={'token_usage': {'completion_tokens': 13, 'prompt_tokens': 35, 'total_tokens': 48}, 'model_name': 'gpt-4o-mini', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-9f90291b-4df9-41dc-9ecf-1ee1081f4490-0', usage_metadata={'input_tokens': 35, 'output_tokens': 13, 'total_tokens': 48})" + "AIMessage(content='Your name is Bob! How can I help you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 12, 'prompt_tokens': 33, 'total_tokens': 45, 'completion_tokens_details': {'reasoning_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_1bb46167f9', 'finish_reason': 'stop', 'logprobs': None}, id='run-c164c5a1-d85f-46ee-ba8a-bb511cfb0e51-0', usage_metadata={'input_tokens': 33, 'output_tokens': 12, 'total_tokens': 45})" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -238,30 +246,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Message History\n", + "## Message persistence\n", "\n", - "We can use a Message History class to wrap our model and make it stateful.\n", - "This will keep track of inputs and outputs of the model, and store them in some datastore.\n", - "Future interactions will then load those messages and pass them into the chain as part of the input.\n", - "Let's see how to use this!\n", + "[LangGraph](https://langchain-ai.github.io/langgraph/) implements a built-in persistence layer, making it ideal for chat applications that support multiple conversational turns.\n", "\n", - "First, let's make sure to install `langchain-community`, as we will be using an integration in there to store message history." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install langchain_community" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "After that, we can import the relevant classes and set up our chain which wraps the model and adds in this message history. A key part here is the function we pass into as the `get_session_history`. This function is expected to take in a `session_id` and return a Message History object. This `session_id` is used to distinguish between separate conversations, and should be passed in as part of the config when calling the new chain (we'll show how to do that)." + "Wrapping our chat model in a minimal LangGraph application allows us to automatically persist the message history, simplifying the development of multi-turn applications.\n", + "\n", + "LangGraph comes with a simple in-memory checkpointer, which we use below. See its [documentation](https://langchain-ai.github.io/langgraph/concepts/persistence/) for more detail, including how to use different persistence backends (e.g., SQLite or Postgres)." ] }, { @@ -270,29 +261,33 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_core.chat_history import (\n", - " BaseChatMessageHistory,\n", - " InMemoryChatMessageHistory,\n", - ")\n", - "from langchain_core.runnables.history import RunnableWithMessageHistory\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import START, MessagesState, StateGraph\n", "\n", - "store = {}\n", + "# Define a new graph\n", + "workflow = StateGraph(state_schema=MessagesState)\n", "\n", "\n", - "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n", - " if session_id not in store:\n", - " store[session_id] = InMemoryChatMessageHistory()\n", - " return store[session_id]\n", + "# Define the function that calls the model\n", + "def call_model(state: MessagesState):\n", + " response = model.invoke(state[\"messages\"])\n", + " return {\"messages\": response}\n", "\n", "\n", - "with_message_history = RunnableWithMessageHistory(model, get_session_history)" + "# Define the (single) node in the graph\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_node(\"model\", call_model)\n", + "\n", + "# Add memory\n", + "memory = MemorySaver()\n", + "app = workflow.compile(checkpointer=memory)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We now need to create a `config` that we pass into the runnable every time. This config contains information that is not part of the input directly, but is still useful. In this case, we want to include a `session_id`. This should look like:" + "We now need to create a `config` that we pass into the runnable every time. This config contains information that is not part of the input directly, but is still useful. In this case, we want to include a `thread_id`. This should look like:" ] }, { @@ -301,7 +296,16 @@ "metadata": {}, "outputs": [], "source": [ - "config = {\"configurable\": {\"session_id\": \"abc2\"}}" + "config = {\"configurable\": {\"thread_id\": \"abc123\"}}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This enables us to support multiple conversation threads with a single application, a common requirement when your application has multiple users.\n", + "\n", + "We can then invoke the application:" ] }, { @@ -310,23 +314,21 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'Hi Bob! How can I assist you today?'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Hi Bob! How can I assist you today?\n" + ] } ], "source": [ - "response = with_message_history.invoke(\n", - " [HumanMessage(content=\"Hi! I'm Bob\")],\n", - " config=config,\n", - ")\n", + "query = \"Hi! I'm Bob.\"\n", "\n", - "response.content" + "input_messages = [HumanMessage(query)]\n", + "output = app.invoke({\"messages\": input_messages}, config)\n", + "output[\"messages\"][-1].pretty_print() # output contains all messages in state" ] }, { @@ -335,30 +337,28 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'Your name is Bob. How can I help you today, Bob?'" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Your name is Bob! How can I help you today?\n" + ] } ], "source": [ - "response = with_message_history.invoke(\n", - " [HumanMessage(content=\"What's my name?\")],\n", - " config=config,\n", - ")\n", + "query = \"What's my name?\"\n", "\n", - "response.content" + "input_messages = [HumanMessage(query)]\n", + "output = app.invoke({\"messages\": input_messages}, config)\n", + "output[\"messages\"][-1].pretty_print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Great! Our chatbot now remembers things about us. If we change the config to reference a different `session_id`, we can see that it starts the conversation fresh." + "Great! Our chatbot now remembers things about us. If we change the config to reference a different `thread_id`, we can see that it starts the conversation fresh." ] }, { @@ -367,25 +367,21 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\"I'm sorry, I cannot determine your name as I am an AI assistant and do not have access to that information.\"" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "I'm sorry, but I don't have access to personal information about you unless you provide it. How can I assist you today?\n" + ] } ], "source": [ - "config = {\"configurable\": {\"session_id\": \"abc3\"}}\n", + "config = {\"configurable\": {\"thread_id\": \"abc234\"}}\n", "\n", - "response = with_message_history.invoke(\n", - " [HumanMessage(content=\"What's my name?\")],\n", - " config=config,\n", - ")\n", - "\n", - "response.content" + "input_messages = [HumanMessage(query)]\n", + "output = app.invoke({\"messages\": input_messages}, config)\n", + "output[\"messages\"][-1].pretty_print()" ] }, { @@ -401,25 +397,21 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'Your name is Bob. How can I assist you today, Bob?'" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Your name is Bob! If there's anything else you'd like to discuss or ask, feel free!\n" + ] } ], "source": [ - "config = {\"configurable\": {\"session_id\": \"abc2\"}}\n", + "config = {\"configurable\": {\"thread_id\": \"abc123\"}}\n", "\n", - "response = with_message_history.invoke(\n", - " [HumanMessage(content=\"What's my name?\")],\n", - " config=config,\n", - ")\n", - "\n", - "response.content" + "input_messages = [HumanMessage(query)]\n", + "output = app.invoke({\"messages\": input_messages}, config)\n", + "output[\"messages\"][-1].pretty_print()" ] }, { @@ -428,18 +420,42 @@ "source": [ "This is how we can support a chatbot having conversations with many users!\n", "\n", - "Right now, all we've done is add a simple persistence layer around the model. We can start to make the more complicated and personalized by adding in a prompt template." + ":::{.callout-tip}\n", + "\n", + "For async support, update the `call_model` node to be an async function and use `.ainvoke` when invoking the application:\n", + "\n", + "```python\n", + "# Async function for node:\n", + "async def call_model(state: MessagesState):\n", + " response = await model.ainvoke(state[\"messages\"])\n", + " return {\"messages\": response}\n", + "\n", + "\n", + "# Define graph as before:\n", + "workflow = StateGraph(state_schema=MessagesState)\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_node(\"model\", call_model)\n", + "app = workflow.compile(checkpointer=MemorySaver())\n", + "\n", + "# Async invocation:\n", + "output = await app.ainvoke({\"messages\": input_messages}, config):\n", + "output[\"messages\"][-1].pretty_print()\n", + "```\n", + "\n", + ":::" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "Right now, all we've done is add a simple persistence layer around the model. We can start to make the more complicated and personalized by adding in a prompt template.\n", + "\n", "## Prompt templates\n", "\n", "Prompt Templates help to turn raw user information into a format that the LLM can work with. In this case, the raw user input is just a message, which we are passing to the LLM. Let's now make that a bit more complicated. First, let's add in a system message with some custom instructions (but still taking messages as input). Next, we'll add in more input besides just the messages.\n", "\n", - "First, let's add in a system message. To do this, we will create a ChatPromptTemplate. We will utilize `MessagesPlaceholder` to pass all the messages in." + "To add in a system message, we will create a `ChatPromptTemplate`. We will utilize `MessagesPlaceholder` to pass all the messages in." ] }, { @@ -454,67 +470,73 @@ " [\n", " (\n", " \"system\",\n", - " \"You are a helpful assistant. Answer all questions to the best of your ability.\",\n", + " \"You talk like a pirate. Answer all questions to the best of your ability.\",\n", " ),\n", " MessagesPlaceholder(variable_name=\"messages\"),\n", " ]\n", - ")\n", - "\n", - "chain = prompt | model" + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note that this slightly changes the input type - rather than pass in a list of messages, we are now passing in a dictionary with a `messages` key where that contains a list of messages." + "We can now update our application to incorporate this template:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Hello Bob! How can I assist you today?'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "response = chain.invoke({\"messages\": [HumanMessage(content=\"hi! I'm bob\")]})\n", + "workflow = StateGraph(state_schema=MessagesState)\n", "\n", - "response.content" + "\n", + "def call_model(state: MessagesState):\n", + " # highlight-start\n", + " chain = prompt | model\n", + " response = chain.invoke(state)\n", + " # highlight-end\n", + " return {\"messages\": response}\n", + "\n", + "\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_node(\"model\", call_model)\n", + "\n", + "memory = MemorySaver()\n", + "app = workflow.compile(checkpointer=memory)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can now wrap this in the same Messages History object as before" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "with_message_history = RunnableWithMessageHistory(chain, get_session_history)" + "We invoke the application in the same way:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Ahoy there, Jim! What brings ye to these treacherous waters today? Be ye seekin’ treasure, tales, or perhaps a bit o’ knowledge? Speak up, matey!\n" + ] + } + ], "source": [ - "config = {\"configurable\": {\"session_id\": \"abc5\"}}" + "config = {\"configurable\": {\"thread_id\": \"abc345\"}}\n", + "query = \"Hi! I'm Jim.\"\n", + "\n", + "input_messages = [HumanMessage(query)]\n", + "output = app.invoke({\"messages\": input_messages}, config)\n", + "output[\"messages\"][-1].pretty_print()" ] }, { @@ -523,48 +545,21 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'Hello, Jim! How can I assist you today?'" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Ye be callin' yerself Jim, if I be hearin' ye correctly! A fine name for a scallywag such as yerself! What else can I do fer ye, me hearty?\n" + ] } ], "source": [ - "response = with_message_history.invoke(\n", - " [HumanMessage(content=\"Hi! I'm Jim\")],\n", - " config=config,\n", - ")\n", + "query = \"What is my name?\"\n", "\n", - "response.content" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Your name is Jim.'" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "response = with_message_history.invoke(\n", - " [HumanMessage(content=\"What's my name?\")],\n", - " config=config,\n", - ")\n", - "\n", - "response.content" + "input_messages = [HumanMessage(query)]\n", + "output = app.invoke({\"messages\": input_messages}, config)\n", + "output[\"messages\"][-1].pretty_print()" ] }, { @@ -576,7 +571,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -588,16 +583,51 @@ " ),\n", " MessagesPlaceholder(variable_name=\"messages\"),\n", " ]\n", - ")\n", - "\n", - "chain = prompt | model" + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note that we have added a new `language` input to the prompt. We can now invoke the chain and pass in a language of our choice." + "Note that we have added a new `language` input to the prompt. Our application now has two parameters-- the input `messages` and `language`. We should update our application's state to reflect this:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Sequence\n", + "\n", + "from langchain_core.messages import BaseMessage\n", + "from langgraph.graph.message import add_messages\n", + "from typing_extensions import Annotated, TypedDict\n", + "\n", + "\n", + "# highlight-next-line\n", + "class State(TypedDict):\n", + " # highlight-next-line\n", + " messages: Annotated[Sequence[BaseMessage], add_messages]\n", + " # highlight-next-line\n", + " language: str\n", + "\n", + "\n", + "workflow = StateGraph(state_schema=State)\n", + "\n", + "\n", + "def call_model(state: State):\n", + " chain = prompt | model\n", + " response = chain.invoke(state)\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_node(\"model\", call_model)\n", + "\n", + "memory = MemorySaver()\n", + "app = workflow.compile(checkpointer=memory)" ] }, { @@ -606,108 +636,67 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'¡Hola, Bob! ¿En qué puedo ayudarte hoy?'" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "¡Hola, Bob! ¿Cómo puedo ayudarte hoy?\n" + ] } ], "source": [ - "response = chain.invoke(\n", - " {\"messages\": [HumanMessage(content=\"hi! I'm bob\")], \"language\": \"Spanish\"}\n", - ")\n", + "config = {\"configurable\": {\"thread_id\": \"abc456\"}}\n", + "query = \"Hi! I'm Bob.\"\n", + "language = \"Spanish\"\n", "\n", - "response.content" + "input_messages = [HumanMessage(query)]\n", + "output = app.invoke(\n", + " # highlight-next-line\n", + " {\"messages\": input_messages, \"language\": language},\n", + " config,\n", + ")\n", + "output[\"messages\"][-1].pretty_print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's now wrap this more complicated chain in a Message History class. This time, because there are multiple keys in the input, we need to specify the correct key to use to save the chat history." + "Note that the entire state is persisted, so we can omit parameters like `language` if no changes are desired:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, - "outputs": [], - "source": [ - "with_message_history = RunnableWithMessageHistory(\n", - " chain,\n", - " get_session_history,\n", - " input_messages_key=\"messages\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "config = {\"configurable\": {\"session_id\": \"abc11\"}}" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'¡Hola Todd! ¿En qué puedo ayudarte hoy?'" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Tu nombre es Bob.\n" + ] } ], "source": [ - "response = with_message_history.invoke(\n", - " {\"messages\": [HumanMessage(content=\"hi! I'm todd\")], \"language\": \"Spanish\"},\n", - " config=config,\n", - ")\n", + "query = \"What is my name?\"\n", "\n", - "response.content" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Tu nombre es Todd.'" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "response = with_message_history.invoke(\n", - " {\"messages\": [HumanMessage(content=\"whats my name?\")], \"language\": \"Spanish\"},\n", - " config=config,\n", + "input_messages = [HumanMessage(query)]\n", + "output = app.invoke(\n", + " {\"messages\": input_messages, \"language\": language},\n", + " config,\n", ")\n", - "\n", - "response.content" + "output[\"messages\"][-1].pretty_print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To help you understand what's happening internally, check out [this LangSmith trace](https://smith.langchain.com/public/f48fabb6-6502-43ec-8242-afc352b769ed/r)" + "To help you understand what's happening internally, check out [this LangSmith trace](https://smith.langchain.com/public/15bd8589-005c-4812-b9b9-23e74ba4c3c6/r)." ] }, { @@ -727,22 +716,22 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[SystemMessage(content=\"you're a good assistant\"),\n", - " HumanMessage(content='whats 2 + 2'),\n", - " AIMessage(content='4'),\n", - " HumanMessage(content='thanks'),\n", - " AIMessage(content='no problem!'),\n", - " HumanMessage(content='having fun?'),\n", - " AIMessage(content='yes!')]" + "[SystemMessage(content=\"you're a good assistant\", additional_kwargs={}, response_metadata={}),\n", + " HumanMessage(content='whats 2 + 2', additional_kwargs={}, response_metadata={}),\n", + " AIMessage(content='4', additional_kwargs={}, response_metadata={}),\n", + " HumanMessage(content='thanks', additional_kwargs={}, response_metadata={}),\n", + " AIMessage(content='no problem!', additional_kwargs={}, response_metadata={}),\n", + " HumanMessage(content='having fun?', additional_kwargs={}, response_metadata={}),\n", + " AIMessage(content='yes!', additional_kwargs={}, response_metadata={})]" ] }, - "execution_count": 24, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -780,45 +769,70 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "To use it in our chain, we just need to run the trimmer before we pass the `messages` input to our prompt. \n", + "To use it in our chain, we just need to run the trimmer before we pass the `messages` input to our prompt. " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "workflow = StateGraph(state_schema=State)\n", "\n", + "\n", + "def call_model(state: State):\n", + " chain = prompt | model\n", + " # highlight-start\n", + " trimmed_messages = trimmer.invoke(state[\"messages\"])\n", + " response = chain.invoke(\n", + " {\"messages\": trimmed_messages, \"language\": state[\"language\"]}\n", + " )\n", + " # highlight-end\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_node(\"model\", call_model)\n", + "\n", + "memory = MemorySaver()\n", + "app = workflow.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "Now if we try asking the model our name, it won't know it since we trimmed that part of the chat history:" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 23, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\"I'm sorry, but I don't have access to your personal information. How can I assist you today?\"" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "I don't know your name. If you'd like to share it, feel free!\n" + ] } ], "source": [ - "from operator import itemgetter\n", + "config = {\"configurable\": {\"thread_id\": \"abc567\"}}\n", + "query = \"What is my name?\"\n", + "language = \"English\"\n", "\n", - "from langchain_core.runnables import RunnablePassthrough\n", - "\n", - "chain = (\n", - " RunnablePassthrough.assign(messages=itemgetter(\"messages\") | trimmer)\n", - " | prompt\n", - " | model\n", + "# highlight-next-line\n", + "input_messages = messages + [HumanMessage(query)]\n", + "output = app.invoke(\n", + " {\"messages\": input_messages, \"language\": language},\n", + " config,\n", ")\n", - "\n", - "response = chain.invoke(\n", - " {\n", - " \"messages\": messages + [HumanMessage(content=\"what's my name?\")],\n", - " \"language\": \"English\",\n", - " }\n", - ")\n", - "response.content" + "output[\"messages\"][-1].pretty_print()" ] }, { @@ -830,120 +844,37 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 24, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'You asked \"what\\'s 2 + 2?\"'" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "You asked what 2 + 2 equals.\n" + ] } ], "source": [ - "response = chain.invoke(\n", - " {\n", - " \"messages\": messages + [HumanMessage(content=\"what math problem did i ask\")],\n", - " \"language\": \"English\",\n", - " }\n", + "config = {\"configurable\": {\"thread_id\": \"abc678\"}}\n", + "query = \"What math problem did I ask?\"\n", + "language = \"English\"\n", + "\n", + "input_messages = messages + [HumanMessage(query)]\n", + "output = app.invoke(\n", + " {\"messages\": input_messages, \"language\": language},\n", + " config,\n", ")\n", - "response.content" + "output[\"messages\"][-1].pretty_print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's now wrap this in the Message History" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "with_message_history = RunnableWithMessageHistory(\n", - " chain,\n", - " get_session_history,\n", - " input_messages_key=\"messages\",\n", - ")\n", - "\n", - "config = {\"configurable\": {\"session_id\": \"abc20\"}}" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"I'm sorry, I don't have access to that information. How can I assist you today?\"" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "response = with_message_history.invoke(\n", - " {\n", - " \"messages\": messages + [HumanMessage(content=\"whats my name?\")],\n", - " \"language\": \"English\",\n", - " },\n", - " config=config,\n", - ")\n", - "\n", - "response.content" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As expected, the first message where we stated our name has been trimmed. Plus there's now two new messages in the chat history (our latest question and the latest response). This means that even more information that used to be accessible in our conversation history is no longer available! In this case our initial math question has been trimmed from the history as well, so the model no longer knows about it:" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"You haven't asked a math problem yet. Feel free to ask any math-related question you have, and I'll be happy to help you with it.\"" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "response = with_message_history.invoke(\n", - " {\n", - " \"messages\": [HumanMessage(content=\"what math problem did i ask?\")],\n", - " \"language\": \"English\",\n", - " },\n", - " config=config,\n", - ")\n", - "\n", - "response.content" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you take a look at LangSmith, you can see exactly what is happening under the hood in the [LangSmith trace](https://smith.langchain.com/public/a64b8b7c-1fd6-4dbb-b11a-47cd09a5e4f1/r)." + "If you take a look at LangSmith, you can see exactly what is happening under the hood in the [LangSmith trace](https://smith.langchain.com/public/04402eaa-29e6-4bb1-aa91-885b730b6c21/r)." ] }, { @@ -956,32 +887,41 @@ "\n", "It's actually super easy to do this!\n", "\n", - "All chains expose a `.stream` method, and ones that use message history are no different. We can simply use that method to get back a streaming response." + "By default, `.stream` in our LangGraph application streams application steps-- in this case, the single step of the model response. Setting `stream_mode=\"messages\"` allows us to stream output tokens instead:" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "|Hi| Todd|!| Sure|,| here|'s| a| joke| for| you|:| Why| couldn|'t| the| bicycle| find| its| way| home|?| Because| it| lost| its| bearings|!| 😄||" + "|Hi| Todd|!| Here|’s| a| joke| for| you|:\n", + "\n", + "|Why| did| the| scare|crow| win| an| award|?\n", + "\n", + "|Because| he| was| outstanding| in| his| field|!||" ] } ], "source": [ - "config = {\"configurable\": {\"session_id\": \"abc15\"}}\n", - "for r in with_message_history.stream(\n", - " {\n", - " \"messages\": [HumanMessage(content=\"hi! I'm todd. tell me a joke\")],\n", - " \"language\": \"English\",\n", - " },\n", - " config=config,\n", + "config = {\"configurable\": {\"thread_id\": \"abc789\"}}\n", + "query = \"Hi I'm Todd, please tell me a joke.\"\n", + "language = \"English\"\n", + "\n", + "input_messages = [HumanMessage(query)]\n", + "# highlight-next-line\n", + "for chunk, metadata in app.stream(\n", + " {\"messages\": input_messages, \"language\": language},\n", + " config,\n", + " # highlight-next-line\n", + " stream_mode=\"messages\",\n", "):\n", - " print(r.content, end=\"|\")" + " if isinstance(chunk, AIMessage): # Filter to just model responses\n", + " print(chunk.content, end=\"|\")" ] }, { @@ -999,7 +939,8 @@ "\n", "- [Streaming](/docs/how_to/streaming): streaming is *crucial* for chat applications\n", "- [How to add message history](/docs/how_to/message_history): for a deeper dive into all things related to message history\n", - "- [How to manage large message history](/docs/how_to/trim_messages/): more techniques for managing a large chat history" + "- [How to manage large message history](/docs/how_to/trim_messages/): more techniques for managing a large chat history\n", + "- [LangGraph main docs](https://langchain-ai.github.io/langgraph/): for more detail on building with LangGraph" ] } ], diff --git a/docs/docs/versions/migrating_chains/conversation_chain.ipynb b/docs/docs/versions/migrating_chains/conversation_chain.ipynb index 9f5eb1ba175..87af17a6558 100644 --- a/docs/docs/versions/migrating_chains/conversation_chain.ipynb +++ b/docs/docs/versions/migrating_chains/conversation_chain.ipynb @@ -9,13 +9,13 @@ "\n", "[`ConversationChain`](https://python.langchain.com/api_reference/langchain/chains/langchain.chains.conversation.base.ConversationChain.html) incorporated a memory of previous messages to sustain a stateful conversation.\n", "\n", - "Some advantages of switching to the LCEL implementation are:\n", + "Some advantages of switching to the Langgraph implementation are:\n", "\n", "- Innate support for threads/separate sessions. To make this work with `ConversationChain`, you'd need to instantiate a separate memory class outside the chain.\n", "- More explicit parameters. `ConversationChain` contains a hidden default prompt, which can cause confusion.\n", "- Streaming support. `ConversationChain` only supports streaming via callbacks.\n", "\n", - "`RunnableWithMessageHistory` implements sessions via configuration parameters. It should be instantiated with a callable that returns a [chat message history](https://python.langchain.com/api_reference/core/chat_history/langchain_core.chat_history.BaseChatMessageHistory.html). By default, it expects this function to take a single argument `session_id`." + "Langgraph's [checkpointing](https://langchain-ai.github.io/langgraph/how-tos/persistence/) system supports multiple threads or sessions, which can be specified via the `\"thread_id\"` key in its configuration parameters." ] }, { @@ -61,9 +61,9 @@ { "data": { "text/plain": [ - "{'input': 'how are you?',\n", + "{'input': \"I'm Bob, how are you?\",\n", " 'history': '',\n", - " 'response': \"Arr matey, I be doin' well on the high seas, plunderin' and pillagin' as usual. How be ye?\"}" + " 'response': \"Arrr matey, I be a pirate sailin' the high seas. What be yer business with me?\"}" ] }, "execution_count": 2, @@ -93,7 +93,30 @@ " prompt=prompt,\n", ")\n", "\n", - "chain({\"input\": \"how are you?\"})" + "chain({\"input\": \"I'm Bob, how are you?\"})" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "53f2c723-178f-470a-8147-54e7cb982211", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'input': 'What is my name?',\n", + " 'history': \"Human: I'm Bob, how are you?\\nAI: Arrr matey, I be a pirate sailin' the high seas. What be yer business with me?\",\n", + " 'response': 'Your name be Bob, matey.'}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain({\"input\": \"What is my name?\"})" ] }, { @@ -103,111 +126,110 @@ "source": [ "\n", "\n", - "## LCEL\n", + "## Langgraph\n", "\n", "
" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "666c92a0-b555-4418-a465-6490c1b92570", + "execution_count": 4, + "id": "a59b910c-0d02-41aa-bc99-441f11989cf8", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Arr, me matey! I be doin' well, sailin' the high seas and searchin' for treasure. How be ye?\"" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "from langchain_core.chat_history import InMemoryChatMessageHistory\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.runnables.history import RunnableWithMessageHistory\n", + "import uuid\n", + "\n", "from langchain_openai import ChatOpenAI\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import START, MessagesState, StateGraph\n", "\n", - "prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", \"You are a pirate. Answer the following questions as best you can.\"),\n", - " (\"placeholder\", \"{chat_history}\"),\n", - " (\"human\", \"{input}\"),\n", - " ]\n", - ")\n", + "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", "\n", - "history = InMemoryChatMessageHistory()\n", + "# Define a new graph\n", + "workflow = StateGraph(state_schema=MessagesState)\n", "\n", "\n", - "def get_history():\n", - " return history\n", + "# Define the function that calls the model\n", + "def call_model(state: MessagesState):\n", + " response = model.invoke(state[\"messages\"])\n", + " return {\"messages\": response}\n", "\n", "\n", - "chain = prompt | ChatOpenAI() | StrOutputParser()\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_node(\"model\", call_model)\n", "\n", - "wrapped_chain = RunnableWithMessageHistory(\n", - " chain,\n", - " get_history,\n", - " history_messages_key=\"chat_history\",\n", - ")\n", + "# Add memory\n", + "memory = MemorySaver()\n", + "app = workflow.compile(checkpointer=memory)\n", "\n", - "wrapped_chain.invoke({\"input\": \"how are you?\"})" - ] - }, - { - "cell_type": "markdown", - "id": "6b386ce6-895e-442c-88f3-7bec0ab9f401", - "metadata": {}, - "source": [ - "The above example uses the same `history` for all sessions. The example below shows how to use a different chat history for each session." + "\n", + "# The thread id is a unique key that identifies\n", + "# this particular conversation.\n", + "# We'll just generate a random uuid here.\n", + "thread_id = uuid.uuid4()\n", + "config = {\"configurable\": {\"thread_id\": thread_id}}" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "96152263-98d7-4e06-8c73-d0c0abf3e8e9", + "execution_count": 5, + "id": "3a9df4bb-e804-4373-9a15-a29dc0371595", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'Ahoy there, me hearty! What can this old pirate do for ye today?'" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "I'm Bob, how are you?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Ahoy, Bob! I be feelin' as lively as a ship in full sail! How be ye on this fine day?\n" + ] } ], "source": [ - "from langchain_core.chat_history import BaseChatMessageHistory\n", - "from langchain_core.runnables.history import RunnableWithMessageHistory\n", + "query = \"I'm Bob, how are you?\"\n", "\n", - "store = {}\n", + "input_messages = [\n", + " {\n", + " \"role\": \"system\",\n", + " \"content\": \"You are a pirate. Answer the following questions as best you can.\",\n", + " },\n", + " {\"role\": \"user\", \"content\": query},\n", + "]\n", + "for event in app.stream({\"messages\": input_messages}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d3f77e69-fa3d-496c-968c-86371e1e8cf1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "What is my name?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Ye be callin' yerself Bob, I reckon! A fine name for a swashbuckler like yerself!\n" + ] + } + ], + "source": [ + "query = \"What is my name?\"\n", "\n", - "\n", - "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n", - " if session_id not in store:\n", - " store[session_id] = InMemoryChatMessageHistory()\n", - " return store[session_id]\n", - "\n", - "\n", - "chain = prompt | ChatOpenAI() | StrOutputParser()\n", - "\n", - "wrapped_chain = RunnableWithMessageHistory(\n", - " chain,\n", - " get_session_history,\n", - " history_messages_key=\"chat_history\",\n", - ")\n", - "\n", - "wrapped_chain.invoke(\n", - " {\"input\": \"Hello!\"},\n", - " config={\"configurable\": {\"session_id\": \"abc123\"}},\n", - ")" + "input_messages = [{\"role\": \"user\", \"content\": query}]\n", + "for event in app.stream({\"messages\": input_messages}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" ] }, {