From ee32369265103ac64b412a73a758ae0d1649cafe Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Thu, 30 May 2024 11:26:41 -0700 Subject: [PATCH] core[patch]: fix runnable history and add docs (#22283) --- docs/docs/how_to/message_history.ipynb | 1081 +++++++++-------- docs/static/img/message_history.png | Bin 0 -> 40253 bytes libs/core/langchain_core/runnables/history.py | 21 +- libs/core/langchain_core/tracers/base.py | 14 +- .../core/langchain_core/tracers/log_stream.py | 2 +- .../langchain_core/tracers/root_listeners.py | 2 +- libs/core/tests/unit_tests/fake/memory.py | 2 + 7 files changed, 630 insertions(+), 492 deletions(-) create mode 100644 docs/static/img/message_history.png diff --git a/docs/docs/how_to/message_history.ipynb b/docs/docs/how_to/message_history.ipynb index dd343fc2661..fbc88b00b22 100644 --- a/docs/docs/how_to/message_history.ipynb +++ b/docs/docs/how_to/message_history.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "6a4becbd-238e-4c1d-a02d-08e61fbc3763", + "id": "f47033eb", "metadata": {}, "source": [ "# How to add message history\n", @@ -18,9 +18,106 @@ "\n", ":::\n", "\n", - "Passing conversation state into and out a chain is vital when building a chatbot. The [`RunnableWithMessageHistory`](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.history.RunnableWithMessageHistory.html#langchain_core.runnables.history.RunnableWithMessageHistory) class lets us add message history to certain types of chains. It wraps another Runnable and manages the chat message history for it.\n", + "Passing conversation state into and out a chain is vital when building a chatbot. The [`RunnableWithMessageHistory`](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.history.RunnableWithMessageHistory.html#langchain_core.runnables.history.RunnableWithMessageHistory) class lets us add message history to certain types of chains. It wraps another Runnable and manages the chat message history for it. Specifically, it loads previous messages in the conversation BEFORE passing it to the Runnable, and it saves the generated response as a message AFTER calling the runnable. This class also enables multiple conversations by saving each conversation with a `session_id` - it then expects a `session_id` to be passed in the config when calling the runnable, and uses that to look up the relevant conversation history.\n", "\n", - "Specifically, it can be used for any Runnable that takes as input one of:\n", + "![index_diagram](../../static/img/message_history.png)\n", + "\n", + "In practice this looks something like:\n", + "\n", + "```python\n", + "from langchain_core.runnables.history import RunnableWithMessageHistory\n", + "\n", + "\n", + "with_message_history = RunnableWithMessageHistory(\n", + " # The underlying runnable\n", + " runnable, \n", + " # A function that takes in a session id and returns a memory object\n", + " get_session_history, \n", + " # Other parameters that may be needed to align the inputs/outputs\n", + " # of the Runnable with the memory object\n", + " ... \n", + ")\n", + "\n", + "with_message_history.invoke(\n", + " # The same input as before\n", + " {\"ability\": \"math\", \"input\": \"What does cosine mean?\"},\n", + " # Configuration specifying the `session_id`,\n", + " # which controls which conversation to load\n", + " config={\"configurable\": {\"session_id\": \"abc123\"}},\n", + ")\n", + "```\n", + "\n", + "\n", + "In order to properly set this up there are two main things to consider:\n", + "\n", + "1. How to store and load messages? (this is `get_session_history` in the example above)\n", + "2. What is the underlying Runnable you are wrapping and what are its inputs/outputs? (this is `runnable` in the example above, as well any additional parameters you pass to `RunnableWithMessageHistory` to align the inputs/outputs)\n", + "\n", + "Let's walk through these pieces (and more) below." + ] + }, + { + "cell_type": "markdown", + "id": "734123cb", + "metadata": {}, + "source": [ + "## How to store and load messages\n", + "\n", + "A key part of this is storing and loading messages.\n", + "When constructing `RunnableWithMessageHistory` you need to pass in a `get_session_history` function.\n", + "This function should take in a `session_id` and return a `BaseChatMessageHistory` object.\n", + "\n", + "**What is `session_id`?** \n", + "\n", + "`session_id` is an identifier for the session (conversation) thread that these input messages correspond to. This allows you to maintain several conversations/threads with the same chain at the same time.\n", + "\n", + "**What is `BaseChatMessageHistory`?** \n", + "\n", + "`BaseChatMessageHistory` is a class that can load and save message objects. It will be called by `RunnableWithMessageHistory` to do exactly that. These classes are usually initialized with a session id.\n", + "\n", + "Let's create a `get_session_history` object to use for this example. To keep things simple, we will use a simple SQLiteMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e8210560", + "metadata": {}, + "outputs": [], + "source": [ + "! rm memory.db" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "27f36241", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.chat_message_histories import SQLChatMessageHistory\n", + "\n", + "\n", + "def get_session_history(session_id):\n", + " return SQLChatMessageHistory(session_id, \"sqlite:///memory.db\")" + ] + }, + { + "cell_type": "markdown", + "id": "c200cb3a", + "metadata": {}, + "source": [ + "Check out the [memory integrations](https://integrations.langchain.com/memory) page for implementations of chat message histories using other providers (Redis, Postgres, etc)." + ] + }, + { + "cell_type": "markdown", + "id": "a531da5e", + "metadata": {}, + "source": [ + "## What is the runnable you are trying wrap?\n", + "\n", + "`RunnableWithMessageHistory` can only wrap certain types of Runnables. Specifically, it can be used for any Runnable that takes as input one of:\n", "\n", "* a sequence of [`BaseMessages`](/docs/concepts/#message-types)\n", "* a dict with a key that takes a sequence of `BaseMessages`\n", @@ -32,7 +129,17 @@ "* a sequence of `BaseMessage`\n", "* a dict with a key that contains a sequence of `BaseMessage`\n", "\n", - "Let's take a look at some examples to see how it works. First we construct a runnable (which here accepts a dict as input and returns a message as output):\n", + "Let's take a look at some examples to see how it works. " + ] + }, + { + "cell_type": "markdown", + "id": "6a4becbd-238e-4c1d-a02d-08e61fbc3763", + "metadata": {}, + "source": [ + "### Setup\n", + "\n", + "First we construct a runnable (which here accepts a dict as input and returns a message as output):\n", "\n", "```{=mdx}\n", "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", @@ -45,7 +152,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "id": "6489f585", "metadata": {}, "outputs": [], @@ -53,24 +160,173 @@ "# | output: false\n", "# | echo: false\n", "\n", - "%pip install -qU langchain langchain_anthropic\n", + "# %pip install -qU langchain langchain_anthropic\n", "\n", - "import os\n", - "from getpass import getpass\n", + "# import os\n", + "# from getpass import getpass\n", "\n", + "# os.environ[\"ANTHROPIC_API_KEY\"] = getpass()\n", "from langchain_anthropic import ChatAnthropic\n", "\n", - "os.environ[\"ANTHROPIC_API_KEY\"] = getpass()\n", - "\n", "model = ChatAnthropic(model=\"claude-3-haiku-20240307\", temperature=0)" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "id": "2ed413b4-33a1-48ee-89b0-2d4917ec101a", "metadata": {}, "outputs": [], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "from langchain_core.runnables.history import RunnableWithMessageHistory" + ] + }, + { + "cell_type": "markdown", + "id": "e8816b01", + "metadata": {}, + "source": [ + "### Messages input, message(s) output\n", + "\n", + "The simplest form is just adding memory to a ChatModel.\n", + "ChatModels accept a list of messages as input and output a message.\n", + "This makes it very easy to use `RunnableWithMessageHistory` - no additional configuration is needed!" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0521d551", + "metadata": {}, + "outputs": [], + "source": [ + "runnable_with_history = RunnableWithMessageHistory(\n", + " model,\n", + " get_session_history,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d5142e1a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content=\"It's nice to meet you, Bob! I'm Claude, an AI assistant created by Anthropic. How can I help you today?\", response_metadata={'id': 'msg_01UHCCMiZz9yNYjt41xUJrtk', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 32}}, id='run-55f6a451-606b-4e04-9e39-e03b81035c1f-0', usage_metadata={'input_tokens': 12, 'output_tokens': 32, 'total_tokens': 44})" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " [HumanMessage(content=\"hi - im bob!\")],\n", + " config={\"configurable\": {\"session_id\": \"1\"}},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "768e0c12", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='I\\'m afraid I don\\'t actually know your name - you introduced yourself as Bob, but I don\\'t have any other information about your identity. As an AI assistant, I don\\'t have a way to independently verify people\\'s names or identities. I\\'m happy to continue our conversation, but I\\'ll just refer to you as \"Bob\" since that\\'s the name you provided.', response_metadata={'id': 'msg_018L96tAxiexMKsHBQz22CcE', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 52, 'output_tokens': 80}}, id='run-7399ddb5-bb06-444b-bfb2-2f65674105dd-0', usage_metadata={'input_tokens': 52, 'output_tokens': 80, 'total_tokens': 132})" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " [HumanMessage(content=\"whats my name?\")],\n", + " config={\"configurable\": {\"session_id\": \"1\"}},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9d942227", + "metadata": {}, + "source": [ + ":::info\n", + "\n", + "Note that in this case the context is preserved via the chat history for the provided `session_id`, so the model knows the users name.\n", + "\n", + ":::\n", + "\n", + "We can now try this with a new session id and see that it does not remember." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "addddd03", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content=\"I'm afraid I don't actually know your name. As an AI assistant, I don't have personal information about you unless you provide it to me directly.\", response_metadata={'id': 'msg_01LhbWu7mSKTvKAx7iQpMPzd', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 35}}, id='run-cf86cad2-21f2-4525-afc8-09bfd1e8af70-0', usage_metadata={'input_tokens': 12, 'output_tokens': 35, 'total_tokens': 47})" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " [HumanMessage(content=\"whats my name?\")],\n", + " config={\"configurable\": {\"session_id\": \"1a\"}},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8b26a0c0", + "metadata": {}, + "source": [ + ":::info \n", + "\n", + "When we pass a different `session_id`, we start a new chat history, so the model does not know what the user's name is. \n", + "\n", + ":::" + ] + }, + { + "cell_type": "markdown", + "id": "e5bb5c7c", + "metadata": {}, + "source": [ + "### Dictionary input, message(s) output\n", + "\n", + "Besides just wrapping a raw model, the next step up is wrapping a prompt + LLM. This now changes the input to be a **dictionary** (because the input to a prompt is a dictionary). This adds two bits of complication.\n", + "\n", + "First: a dictionary can have multiple keys, but we only want to save ONE as input. In order to do this, we now now need to specify a key to save as the input.\n", + "\n", + "Second: once we load the messages, we need to know how to save them to the dictionary. That equates to know which key in the dictionary to save them in. Therefore, we need to specify a key to save the loaded messages in.\n", + "\n", + "Putting it all together, that ends up looking something like:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "34edd990", + "metadata": {}, + "outputs": [], "source": [ "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", "\n", @@ -78,62 +334,16 @@ " [\n", " (\n", " \"system\",\n", - " \"You're an assistant who's good at {ability}. Respond in 20 words or fewer\",\n", + " \"You're an assistant who speaks in {language}. Respond in 20 words or fewer\",\n", " ),\n", " MessagesPlaceholder(variable_name=\"history\"),\n", " (\"human\", \"{input}\"),\n", " ]\n", ")\n", - "runnable = prompt | model" - ] - }, - { - "cell_type": "markdown", - "id": "9fd175e1-c7b8-4929-a57e-3331865fe7aa", - "metadata": {}, - "source": [ - "To manage the message history, we will need:\n", - "1. This runnable;\n", - "2. A callable that returns an instance of `BaseChatMessageHistory`.\n", "\n", - "Check out the [memory integrations](https://integrations.langchain.com/memory) page for implementations of chat message histories using Redis and other providers. Here we demonstrate using an in-memory `ChatMessageHistory` as well as more persistent storage using `RedisChatMessageHistory`." - ] - }, - { - "cell_type": "markdown", - "id": "3d83adad-9672-496d-9f25-5747e7b8c8bb", - "metadata": {}, - "source": [ - "## In-memory\n", + "runnable = prompt | model\n", "\n", - "Below we show a simple example in which the chat history lives in memory, in this case via a global Python dict.\n", - "\n", - "We construct a callable `get_session_history` that references this dict to return an instance of `ChatMessageHistory`. The arguments to the callable can be specified by passing a configuration to the `RunnableWithMessageHistory` at runtime. By default, the configuration parameter is expected to be a single string `session_id`. This can be adjusted via the `history_factory_config` kwarg.\n", - "\n", - "Using the single-parameter default:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "54348d02-d8ee-440c-bbf9-41bc0fbbc46c", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.chat_message_histories import ChatMessageHistory\n", - "from langchain_core.chat_history import BaseChatMessageHistory\n", - "from langchain_core.runnables.history import RunnableWithMessageHistory\n", - "\n", - "store = {}\n", - "\n", - "\n", - "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n", - " if session_id not in store:\n", - " store[session_id] = ChatMessageHistory()\n", - " return store[session_id]\n", - "\n", - "\n", - "with_message_history = RunnableWithMessageHistory(\n", + "runnable_with_history = RunnableWithMessageHistory(\n", " runnable,\n", " get_session_history,\n", " input_messages_key=\"input\",\n", @@ -143,7 +353,7 @@ }, { "cell_type": "markdown", - "id": "01acb505-3fd3-4ab4-9f04-5ea07e81542e", + "id": "c0baa075", "metadata": {}, "source": [ ":::info\n", @@ -153,114 +363,378 @@ ":::" ] }, - { - "cell_type": "markdown", - "id": "35222c30", - "metadata": {}, - "source": [ - "When invoking this new runnable, we specify the corresponding chat history via a configuration parameter:" - ] - }, { "cell_type": "code", - "execution_count": 4, - "id": "01384412-f08e-4634-9edb-3f46f475b582", + "execution_count": 16, + "id": "5877544f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content='Cosine is a trigonometric function that represents the ratio of the adjacent side to the hypotenuse of a right triangle.', response_metadata={'id': 'msg_01DH8iRBELVbF3sqM8U5sk8A', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 32, 'output_tokens': 31}}, id='run-e07fc012-a4f6-4e47-8ef8-250f296eba5b-0')" + "AIMessage(content='Ciao Bob! È un piacere conoscerti. Come stai oggi?', response_metadata={'id': 'msg_0121ADUEe4G1hMC6zbqFWofr', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 29, 'output_tokens': 23}}, id='run-246a70df-aad6-43d6-a7e8-166d96e0d67e-0', usage_metadata={'input_tokens': 29, 'output_tokens': 23, 'total_tokens': 52})" ] }, - "execution_count": 4, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "with_message_history.invoke(\n", - " {\"ability\": \"math\", \"input\": \"What does cosine mean?\"},\n", - " config={\"configurable\": {\"session_id\": \"abc123\"}},\n", + "runnable_with_history.invoke(\n", + " {\"language\": \"italian\", \"input\": \"hi im bob!\"},\n", + " config={\"configurable\": {\"session_id\": \"2\"}},\n", ")" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "954688a2-9a3f-47ee-a9e8-fa0c83e69477", + "execution_count": 17, + "id": "8605c2b1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content='The inverse of the cosine function is called the arccosine or inverse cosine.', response_metadata={'id': 'msg_015TeeRQBvTvc7XG1JxYqZyq', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 72, 'output_tokens': 22}}, id='run-32ae22ea-3b2f-4d38-8c8a-cb8702e2f3e7-0')" + "AIMessage(content='Bob, il tuo nome è Bob.', response_metadata={'id': 'msg_01EDUZG6nRLGeti9KhFN5cek', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 60, 'output_tokens': 12}}, id='run-294b4a72-81bc-4c43-b199-3aafdff87cb3-0', usage_metadata={'input_tokens': 60, 'output_tokens': 12, 'total_tokens': 72})" ] }, - "execution_count": 5, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Remembers\n", - "with_message_history.invoke(\n", - " {\"ability\": \"math\", \"input\": \"What is its inverse called?\"},\n", - " config={\"configurable\": {\"session_id\": \"abc123\"}},\n", + "runnable_with_history.invoke(\n", + " {\"language\": \"italian\", \"input\": \"whats my name?\"},\n", + " config={\"configurable\": {\"session_id\": \"2\"}},\n", ")" ] }, { "cell_type": "markdown", - "id": "e0c651e5", + "id": "3ab7c09f", "metadata": {}, "source": [ ":::info\n", "\n", - "Note that in this case the context is preserved via the chat history for the provided `session_id`, so the model knows that \"it\" refers to \"cosine\" in this case.\n", + "Note that in this case the context is preserved via the chat history for the provided `session_id`, so the model knows the users name.\n", + "\n", + ":::\n", + "\n", + "We can now try this with a new session id and see that it does not remember." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "c7ddad6b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='Mi dispiace, non so il tuo nome. Come posso aiutarti?', response_metadata={'id': 'msg_01Lyd9FAGQJTxxAZoFi3sQpQ', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 30, 'output_tokens': 23}}, id='run-19a82197-3b1c-4b5f-a68d-f91f4a2ba523-0', usage_metadata={'input_tokens': 30, 'output_tokens': 23, 'total_tokens': 53})" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " {\"language\": \"italian\", \"input\": \"whats my name?\"},\n", + " config={\"configurable\": {\"session_id\": \"2a\"}},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a05e6c12", + "metadata": {}, + "source": [ + ":::info \n", + "\n", + "When we pass a different `session_id`, we start a new chat history, so the model does not know what the user's name is. \n", "\n", ":::" ] }, { "cell_type": "markdown", - "id": "a44f8d5f", + "id": "717440a9", "metadata": {}, "source": [ - "Now let's try a different `session_id`" + "### Messages input, dict output\n", + "\n", + "This format is useful when you are using a model to generate one key in a dictionary." ] }, { "cell_type": "code", - "execution_count": 6, - "id": "39350d7c-2641-4744-bc2a-fd6a57c4ea90", + "execution_count": 20, + "id": "80b8efb0", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AIMessage(content='The inverse of a function is the function that undoes the original function.', response_metadata={'id': 'msg_01M8WbHWg2sjWTz3m3NKqZuF', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 32, 'output_tokens': 18}}, id='run-b64c73d6-03ee-4b0a-85e0-34beb45408d4-0')" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "# New session_id --> does not remember.\n", - "with_message_history.invoke(\n", - " {\"ability\": \"math\", \"input\": \"What is its inverse called?\"},\n", - " config={\"configurable\": {\"session_id\": \"def234\"}},\n", + "from langchain_core.messages import HumanMessage\n", + "from langchain_core.runnables import RunnableParallel\n", + "\n", + "chain = RunnableParallel({\"output_message\": model})\n", + "\n", + "\n", + "runnable_with_history = RunnableWithMessageHistory(\n", + " chain,\n", + " get_session_history,\n", + " output_messages_key=\"output_message\",\n", ")" ] }, { "cell_type": "markdown", - "id": "5416e195", + "id": "9040c535", "metadata": {}, "source": [ - "When we pass a different `session_id`, we start a new chat history, so the model does not know what \"it\" refers to." + ":::info\n", + "\n", + "Note that we've specified `output_messages_key` (the key to be treated as the output to save).\n", + "\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "8b26a209", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'output_message': AIMessage(content=\"It's nice to meet you, Bob! I'm Claude, an AI assistant created by Anthropic. How can I help you today?\", response_metadata={'id': 'msg_01WWJSyUyGGKuBqTs3h18ZMM', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 32}}, id='run-0f50cb43-a734-447c-b535-07c615a0984c-0', usage_metadata={'input_tokens': 12, 'output_tokens': 32, 'total_tokens': 44})}" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " [HumanMessage(content=\"hi - im bob!\")],\n", + " config={\"configurable\": {\"session_id\": \"3\"}},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "743edcf8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'output_message': AIMessage(content='I\\'m afraid I don\\'t actually know your name - you introduced yourself as Bob, but I don\\'t have any other information about your identity. As an AI assistant, I don\\'t have a way to independently verify people\\'s names or identities. I\\'m happy to continue our conversation, but I\\'ll just refer to you as \"Bob\" since that\\'s the name you provided.', response_metadata={'id': 'msg_01TEGrhfLXTwo36rC7svdTy4', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 52, 'output_tokens': 80}}, id='run-178e8f3f-da21-430d-9edc-ef07797a5e2d-0', usage_metadata={'input_tokens': 52, 'output_tokens': 80, 'total_tokens': 132})}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " [HumanMessage(content=\"whats my name?\")],\n", + " config={\"configurable\": {\"session_id\": \"3\"}},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "81efb7f1", + "metadata": {}, + "source": [ + ":::info\n", + "\n", + "Note that in this case the context is preserved via the chat history for the provided `session_id`, so the model knows the users name.\n", + "\n", + ":::\n", + "\n", + "We can now try this with a new session id and see that it does not remember." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "b8b04907", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'output_message': AIMessage(content=\"I'm afraid I don't actually know your name. As an AI assistant, I don't have personal information about you unless you provide it to me directly.\", response_metadata={'id': 'msg_0118ZBudDXAC9P6smf91NhCX', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 35}}, id='run-deb14a3a-0336-42b4-8ace-ad1e52ca5910-0', usage_metadata={'input_tokens': 12, 'output_tokens': 35, 'total_tokens': 47})}" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " [HumanMessage(content=\"whats my name?\")],\n", + " config={\"configurable\": {\"session_id\": \"3a\"}},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6716a068", + "metadata": {}, + "source": [ + ":::info \n", + "\n", + "When we pass a different `session_id`, we start a new chat history, so the model does not know what the user's name is. \n", + "\n", + ":::" + ] + }, + { + "cell_type": "markdown", + "id": "ec4187d0", + "metadata": {}, + "source": [ + "### Dict with single key for all messages input, messages output\n", + "\n", + "This is a specific case of \"Dictionary input, message(s) output\". In this situation, because there is only a single key we don't need to specify as much - we only need to specify the `input_messages_key`." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "7530c4ed", + "metadata": {}, + "outputs": [], + "source": [ + "from operator import itemgetter\n", + "\n", + "runnable_with_history = RunnableWithMessageHistory(\n", + " itemgetter(\"input_messages\") | model,\n", + " get_session_history,\n", + " input_messages_key=\"input_messages\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "def75152", + "metadata": {}, + "source": [ + ":::info\n", + "\n", + "Note that we've specified `input_messages_key` (the key to be treated as the latest input message).\n", + "\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "659bc1bf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content=\"It's nice to meet you, Bob! I'm Claude, an AI assistant created by Anthropic. How can I help you today?\", response_metadata={'id': 'msg_01UdD5wz1J5xwoz5D94onaQC', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 32}}, id='run-91bee6eb-0814-4557-ad71-fef9b0270358-0', usage_metadata={'input_tokens': 12, 'output_tokens': 32, 'total_tokens': 44})" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " {\"input_messages\": [HumanMessage(content=\"hi - im bob!\")]},\n", + " config={\"configurable\": {\"session_id\": \"4\"}},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "6da2835e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='I\\'m afraid I don\\'t actually know your name - you introduced yourself as Bob, but I don\\'t have any other information about your identity. As an AI assistant, I don\\'t have a way to independently verify people\\'s names or identities. I\\'m happy to continue our conversation, but I\\'ll just refer to you as \"Bob\" since that\\'s the name you provided.', response_metadata={'id': 'msg_012WUygxBKXcVJPeTW14LNrc', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 52, 'output_tokens': 80}}, id='run-fcbaaa1a-8c33-4eec-b0b0-5b800a47bddd-0', usage_metadata={'input_tokens': 52, 'output_tokens': 80, 'total_tokens': 132})" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " {\"input_messages\": [HumanMessage(content=\"whats my name?\")]},\n", + " config={\"configurable\": {\"session_id\": \"4\"}},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d4c7a6f2", + "metadata": {}, + "source": [ + ":::info\n", + "\n", + "Note that in this case the context is preserved via the chat history for the provided `session_id`, so the model knows the users name.\n", + "\n", + ":::\n", + "\n", + "We can now try this with a new session id and see that it does not remember." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "6cf6abd6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content=\"I'm afraid I don't actually know your name. As an AI assistant, I don't have personal information about you unless you provide it to me directly.\", response_metadata={'id': 'msg_017xW3Ki5y4UBYzCU9Mf1pgM', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 12, 'output_tokens': 35}}, id='run-d2f372f7-3679-4a5c-9331-a55b820ec03e-0', usage_metadata={'input_tokens': 12, 'output_tokens': 35, 'total_tokens': 47})" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runnable_with_history.invoke(\n", + " {\"input_messages\": [HumanMessage(content=\"whats my name?\")]},\n", + " config={\"configurable\": {\"session_id\": \"4a\"}},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9839a6d1", + "metadata": {}, + "source": [ + ":::info \n", + "\n", + "When we pass a different `session_id`, we start a new chat history, so the model does not know what the user's name is. \n", + "\n", + ":::" ] }, { @@ -268,7 +742,7 @@ "id": "a6710e65", "metadata": {}, "source": [ - "### Customization" + "## Customization" ] }, { @@ -281,17 +755,17 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 30, "id": "1c89daee-deff-4fdf-86a3-178f7d8ef536", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content=\"Why can't a bicycle stand up on its own? It's two-tired!\", response_metadata={'id': 'msg_011qHi8pvbNkKhRb9XYRm2kc', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 30, 'output_tokens': 20}}, id='run-5d1d5b5a-ccec-4c2c-b11a-f1953dbe85a3-0')" + "AIMessage(content='Ciao Bob! È un piacere conoscerti. Come stai oggi?', response_metadata={'id': 'msg_016RJebCoiAgWaNcbv9wrMNW', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 29, 'output_tokens': 23}}, id='run-40425414-8f72-47d4-bf1d-a84175d8b3f8-0', usage_metadata={'input_tokens': 29, 'output_tokens': 23, 'total_tokens': 52})" ] }, - "execution_count": 7, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -299,13 +773,9 @@ "source": [ "from langchain_core.runnables import ConfigurableFieldSpec\n", "\n", - "store = {}\n", "\n", - "\n", - "def get_session_history(user_id: str, conversation_id: str) -> BaseChatMessageHistory:\n", - " if (user_id, conversation_id) not in store:\n", - " store[(user_id, conversation_id)] = ChatMessageHistory()\n", - " return store[(user_id, conversation_id)]\n", + "def get_session_history(user_id: str, conversation_id: str):\n", + " return SQLChatMessageHistory(f\"{user_id}--{conversation_id}\", \"sqlite:///memory.db\")\n", "\n", "\n", "with_message_history = RunnableWithMessageHistory(\n", @@ -334,24 +804,24 @@ ")\n", "\n", "with_message_history.invoke(\n", - " {\"ability\": \"jokes\", \"input\": \"Tell me a joke\"},\n", + " {\"language\": \"italian\", \"input\": \"hi im bob!\"},\n", " config={\"configurable\": {\"user_id\": \"123\", \"conversation_id\": \"1\"}},\n", ")" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 32, "id": "4f282883", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content='The joke was about a bicycle not being able to stand up on its own because it\\'s \"two-tired\" (too tired).', response_metadata={'id': 'msg_01LbrkfidZgseBMxxRjQXJQH', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 59, 'output_tokens': 30}}, id='run-8b2ca810-77d7-44b8-b27b-677e0062b19a-0')" + "AIMessage(content='Bob, il tuo nome è Bob.', response_metadata={'id': 'msg_01Kktiy3auFDKESY54KtTWPX', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 60, 'output_tokens': 12}}, id='run-c7768420-3f30-43f5-8834-74b1979630dd-0', usage_metadata={'input_tokens': 60, 'output_tokens': 12, 'total_tokens': 72})" ] }, - "execution_count": 8, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -359,24 +829,24 @@ "source": [ "# remembers\n", "with_message_history.invoke(\n", - " {\"ability\": \"jokes\", \"input\": \"What was the joke about?\"},\n", + " {\"language\": \"italian\", \"input\": \"whats my name?\"},\n", " config={\"configurable\": {\"user_id\": \"123\", \"conversation_id\": \"1\"}},\n", ")" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 33, "id": "fc122c18", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content=\"I'm afraid I don't have enough context to provide a relevant joke. As an AI assistant, I don't actually have pre-programmed jokes. I'd be happy to try generating a humorous response if you provide more details about the context.\", response_metadata={'id': 'msg_01PgSp46hNJnKyNfNKPDauQ9', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 32, 'output_tokens': 54}}, id='run-ed202892-27e4-4da9-a26d-e0dc16b10940-0')" + "AIMessage(content='Mi dispiace, non so il tuo nome. Come posso aiutarti?', response_metadata={'id': 'msg_0178FpbpPNioB7kqvyHk7rjD', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 30, 'output_tokens': 23}}, id='run-df1f1768-aab6-4aec-8bba-e33fc9e90b8d-0', usage_metadata={'input_tokens': 30, 'output_tokens': 23, 'total_tokens': 53})" ] }, - "execution_count": 9, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -384,7 +854,7 @@ "source": [ "# New user_id --> does not remember\n", "with_message_history.invoke(\n", - " {\"ability\": \"jokes\", \"input\": \"What was the joke about?\"},\n", + " {\"language\": \"italian\", \"input\": \"whats my name?\"},\n", " config={\"configurable\": {\"user_id\": \"456\", \"conversation_id\": \"1\"}},\n", ")" ] @@ -396,361 +866,6 @@ "source": [ "Note that in this case the context was preserved for the same `user_id`, but once we changed it, the new chat history was started, even though the `conversation_id` was the same." ] - }, - { - "cell_type": "markdown", - "id": "18f1a459-3f88-4ee6-8542-76a907070dd6", - "metadata": {}, - "source": [ - "### Examples with runnables of different signatures\n", - "\n", - "The above runnable takes a dict as input and returns a BaseMessage. Below we show some alternatives." - ] - }, - { - "cell_type": "markdown", - "id": "48eae1bf-b59d-4a61-8e62-b6dbf667e866", - "metadata": {}, - "source": [ - "#### Messages input, dict output" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "17733d4f-3a32-4055-9d44-5d58b9446a26", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'output_message': AIMessage(content='Simone de Beauvoir was a prominent French existentialist philosopher who had some key beliefs about free will:\\n\\n1. Radical Freedom: De Beauvoir believed that humans have radical freedom - the ability to choose and define themselves through their actions. She rejected determinism and believed that we are not simply products of our biology, upbringing, or social circumstances.\\n\\n2. Ambiguity of the Human Condition: However, de Beauvoir also recognized the ambiguity of the human condition. While we have radical freedom, we are also situated beings constrained by our facticity (our given circumstances and limitations). This creates a tension and anguish in the human experience.\\n\\n3. Responsibility and Bad Faith: With radical freedom comes great responsibility. De Beauvoir criticized \"bad faith\" - the denial or avoidance of this responsibility by making excuses or pretending we lack free will. She believed we must courageously embrace our freedom and the burdens it entails.\\n\\n4. Ethical Engagement: For de Beauvoir, freedom is not just an abstract philosophical concept, but something that must be exercised through ethical engagement with the world and others. Our choices and actions have moral implications that we must grapple with.\\n\\nOverall, de Beauvoir\\'s perspective on free will was grounded in existentialist principles - the belief that we are fundamentally free, yet this freedom is fraught with difficulty and responsibility. Her views emphasized the centrality of human agency and the ethical dimensions of our choices.', response_metadata={'id': 'msg_01QFXHx74GSzcMWnWc8YxYSJ', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 20, 'output_tokens': 324}}, id='run-752513bc-2b4f-4cad-87f0-b96fee6ebe43-0')}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "from langchain_core.runnables import RunnableParallel\n", - "\n", - "chain = RunnableParallel({\"output_message\": model})\n", - "\n", - "\n", - "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n", - " if session_id not in store:\n", - " store[session_id] = ChatMessageHistory()\n", - " return store[session_id]\n", - "\n", - "\n", - "with_message_history = RunnableWithMessageHistory(\n", - " chain,\n", - " get_session_history,\n", - " output_messages_key=\"output_message\",\n", - ")\n", - "\n", - "with_message_history.invoke(\n", - " [HumanMessage(content=\"What did Simone de Beauvoir believe about free will\")],\n", - " config={\"configurable\": {\"session_id\": \"baz\"}},\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "efb57ef5-91f9-426b-84b9-b77f071a9dd7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'output_message': AIMessage(content='Simone de Beauvoir\\'s views on free will were quite similar to those of her long-time partner and fellow existentialist philosopher, Jean-Paul Sartre. There are some key parallels and differences:\\n\\nSimilarities:\\n\\n1. Radical Freedom: Both de Beauvoir and Sartre believed that humans have radical, unconditioned freedom to choose and define themselves.\\n\\n2. Rejection of Determinism: They both rejected deterministic views that see humans as products of their circumstances or nature.\\n\\n3. Emphasis on Responsibility: They agreed that with radical freedom comes great responsibility for one\\'s choices and actions.\\n\\n4. Critique of \"Bad Faith\": Both philosophers criticized the tendency of people to deny or avoid their freedom through self-deception and making excuses.\\n\\nDifferences:\\n\\n1. Gendered Perspectives: While Sartre developed a more gender-neutral existentialist philosophy, de Beauvoir brought a distinctly feminist lens, exploring the unique challenges and experiences of women\\'s freedom.\\n\\n2. Ethical Engagement: De Beauvoir placed more emphasis on the importance of ethical engagement with the world and others, whereas Sartre\\'s focus was more individualistic.\\n\\n3. Ambiguity of the Human Condition: De Beauvoir was more attuned to the ambiguity and tensions inherent in the human condition, whereas Sartre\\'s views were sometimes seen as more absolutist.\\n\\n4. Influence of Phenomenology: De Beauvoir was more influenced by the phenomenological tradition, which shaped her understanding of embodied, situated freedom.\\n\\nOverall, while Sartre and de Beauvoir shared a core existentialist framework, de Beauvoir\\'s unique feminist perspective and emphasis on ethical engagement with others distinguished her views on free will and the human condition.', response_metadata={'id': 'msg_01BEANW4VX6cUWYjkv3CanLz', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 355, 'output_tokens': 388}}, id='run-e786ab3a-1a42-45f3-94a3-f0c591430df3-0')}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "with_message_history.invoke(\n", - " [HumanMessage(content=\"How did this compare to Sartre\")],\n", - " config={\"configurable\": {\"session_id\": \"baz\"}},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a39eac5f-a9d8-4729-be06-5e7faf0c424d", - "metadata": {}, - "source": [ - "#### Messages input, messages output" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "e45bcd95-e31f-4a9a-967a-78f96e8da881", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RunnableWithMessageHistory(bound=RunnableBinding(bound=RunnableBinding(bound=RunnableLambda(_enter_history), config={'run_name': 'load_history'})\n", - "| RunnableBinding(bound=ChatAnthropic(model='claude-3-haiku-20240307', temperature=0.0, anthropic_api_url='https://api.anthropic.com', anthropic_api_key=SecretStr('**********'), _client=, _async_client=), config_factories=[. at 0x106aeef20>]), config={'run_name': 'RunnableWithMessageHistory'}), get_session_history=, history_factory_config=[ConfigurableFieldSpec(id='session_id', annotation=, name='Session ID', description='Unique identifier for a session.', default='', is_shared=True, dependencies=None)])" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "RunnableWithMessageHistory(\n", - " model,\n", - " get_session_history,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "04daa921-a2d1-40f9-8cd1-ae4e9a4163a7", - "metadata": {}, - "source": [ - "#### Dict with single key for all messages input, messages output" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "27157f15-9fb0-4167-9870-f4d7f234b3cb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RunnableWithMessageHistory(bound=RunnableBinding(bound=RunnableBinding(bound=RunnableAssign(mapper={\n", - " input_messages: RunnableBinding(bound=RunnableLambda(_enter_history), config={'run_name': 'load_history'})\n", - "}), config={'run_name': 'insert_history'})\n", - "| RunnableBinding(bound=RunnableLambda(itemgetter('input_messages'))\n", - " | ChatAnthropic(model='claude-3-haiku-20240307', temperature=0.0, anthropic_api_url='https://api.anthropic.com', anthropic_api_key=SecretStr('**********'), _client=, _async_client=), config_factories=[. at 0x106aef560>]), config={'run_name': 'RunnableWithMessageHistory'}), get_session_history=, input_messages_key='input_messages', history_factory_config=[ConfigurableFieldSpec(id='session_id', annotation=, name='Session ID', description='Unique identifier for a session.', default='', is_shared=True, dependencies=None)])" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from operator import itemgetter\n", - "\n", - "RunnableWithMessageHistory(\n", - " itemgetter(\"input_messages\") | model,\n", - " get_session_history,\n", - " input_messages_key=\"input_messages\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "418ca7af-9ed9-478c-8bca-cba0de2ca61e", - "metadata": {}, - "source": [ - "## Persistent storage" - ] - }, - { - "cell_type": "markdown", - "id": "76799a13-d99a-4c4f-91f2-db699e40b8df", - "metadata": {}, - "source": [ - "In many cases it is preferable to persist conversation histories. `RunnableWithMessageHistory` is agnostic as to how the `get_session_history` callable retrieves its chat message histories. See [here](https://github.com/langchain-ai/langserve/blob/main/examples/chat_with_persistence_and_user/server.py) for an example using a local filesystem. Below we demonstrate how one could use Redis. Check out the [memory integrations](https://integrations.langchain.com/memory) page for implementations of chat message histories using other providers." - ] - }, - { - "cell_type": "markdown", - "id": "6bca45e5-35d9-4603-9ca9-6ac0ce0e35cd", - "metadata": {}, - "source": [ - "### Setup\n", - "\n", - "We'll need to install Redis if it's not installed already:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "477d04b3-c2b6-4ba5-962f-492c0d625cd5", - "metadata": {}, - "outputs": [], - "source": [ - "%pip install --upgrade --quiet redis" - ] - }, - { - "cell_type": "markdown", - "id": "6a0ec9e0-7b1c-4c6f-b570-e61d520b47c6", - "metadata": {}, - "source": [ - "Start a local Redis Stack server if we don't have an existing Redis deployment to connect to:\n", - "```bash\n", - "docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "cd6a250e-17fe-4368-a39d-1fe6b2cbde68", - "metadata": {}, - "outputs": [], - "source": [ - "REDIS_URL = \"redis://localhost:6379/0\"" - ] - }, - { - "cell_type": "markdown", - "id": "36f43b87-655c-4f64-aa7b-bd8c1955d8e5", - "metadata": {}, - "source": [ - "### [LangSmith](https://docs.smith.langchain.com)\n", - "\n", - "LangSmith is especially useful for something like message history injection, where it can be hard to otherwise understand what the inputs are to various parts of the chain.\n", - "\n", - "Note that LangSmith is not needed, but it is helpful.\n", - "If you do want to use LangSmith, after you sign up at the link above, make sure to uncoment the below and set your environment variables to start logging traces:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "2afc1556-8da1-4499-ba11-983b66c58b18", - "metadata": {}, - "outputs": [], - "source": [ - "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" - ] - }, - { - "cell_type": "markdown", - "id": "f9d81796-ce61-484c-89e2-6c567d5e54ef", - "metadata": {}, - "source": [ - "Updating the message history implementation just requires us to define a new callable, this time returning an instance of `RedisChatMessageHistory`:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "ca7c64d8-e138-4ef8-9734-f82076c47d80", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.chat_message_histories import RedisChatMessageHistory\n", - "\n", - "\n", - "def get_message_history(session_id: str) -> RedisChatMessageHistory:\n", - " return RedisChatMessageHistory(session_id, url=REDIS_URL)\n", - "\n", - "\n", - "with_message_history = RunnableWithMessageHistory(\n", - " runnable,\n", - " get_message_history,\n", - " input_messages_key=\"input\",\n", - " history_messages_key=\"history\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "37eefdec-9901-4650-b64c-d3c097ed5f4d", - "metadata": {}, - "source": [ - "We can invoke as before:" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "a85bcc22-ca4c-4ad5-9440-f94be7318f3e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AIMessage(content='Cosine is a trigonometric function that represents the ratio of the adjacent side to the hypotenuse of a right triangle.', response_metadata={'id': 'msg_01DwU2BD8KPLoXeZ6bZPqxxJ', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 164, 'output_tokens': 31}}, id='run-c2a443c4-79b1-4b07-bb42-5e9112e5bbfc-0')" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "with_message_history.invoke(\n", - " {\"ability\": \"math\", \"input\": \"What does cosine mean?\"},\n", - " config={\"configurable\": {\"session_id\": \"foobar\"}},\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "ab29abd3-751f-41ce-a1b0-53f6b565e79d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AIMessage(content='The inverse of cosine is called arccosine or inverse cosine.', response_metadata={'id': 'msg_01XYH5iCUokxV1UDhUa8xzna', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 202, 'output_tokens': 19}}, id='run-97dda3a2-01e3-42e5-8241-f948e7535ffc-0')" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "with_message_history.invoke(\n", - " {\"ability\": \"math\", \"input\": \"What's its inverse\"},\n", - " config={\"configurable\": {\"session_id\": \"foobar\"}},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "da3d1feb-b4bb-4624-961c-7db2e1180df7", - "metadata": {}, - "source": [ - ":::tip\n", - "\n", - "[Langsmith trace](https://smith.langchain.com/public/bd73e122-6ec1-48b2-82df-e6483dc9cb63/r)\n", - "\n", - ":::" - ] - }, - { - "cell_type": "markdown", - "id": "61d5115e-64a1-4ad5-b676-8afd4ef6093e", - "metadata": {}, - "source": [ - "Looking at the Langsmith trace for the second call, we can see that when constructing the prompt, a \"history\" variable has been injected which is a list of two messages (our first input and first output)." - ] - }, - { - "cell_type": "markdown", - "id": "fd510b68", - "metadata": {}, - "source": [ - "## Next steps\n", - "\n", - "You have now learned one way to manage message history for a runnable.\n", - "\n", - "To learn more, see the other how-to guides on runnables in this section." - ] } ], "metadata": { @@ -769,7 +884,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.10.1" } }, "nbformat": 4, diff --git a/docs/static/img/message_history.png b/docs/static/img/message_history.png new file mode 100644 index 0000000000000000000000000000000000000000..31f7664d286bf04d75ac72799e00ffcec7767254 GIT binary patch literal 40253 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 literal 0 HcmV?d00001 diff --git a/libs/core/langchain_core/runnables/history.py b/libs/core/langchain_core/runnables/history.py index 3ff6df09d7b..fdc40e55835 100644 --- a/libs/core/langchain_core/runnables/history.py +++ b/libs/core/langchain_core/runnables/history.py @@ -372,6 +372,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): ) -> List[BaseMessage]: from langchain_core.messages import BaseMessage + # If dictionary, try to pluck the single key representing messages if isinstance(input_val, dict): if self.input_messages_key: key = self.input_messages_key @@ -381,13 +382,25 @@ class RunnableWithMessageHistory(RunnableBindingBase): key = "input" input_val = input_val[key] + # If value is a string, convert to a human message if isinstance(input_val, str): from langchain_core.messages import HumanMessage return [HumanMessage(content=input_val)] + # If value is a single message, convert to a list elif isinstance(input_val, BaseMessage): return [input_val] + # If value is a list or tuple... elif isinstance(input_val, (list, tuple)): + # Handle empty case + if len(input_val) == 0: + return list(input_val) + # If is a list of list, then return the first value + # This occurs for chat models - since we batch inputs + if isinstance(input_val[0], list): + if len(input_val) != 1: + raise ValueError() + return input_val[0] return list(input_val) else: raise ValueError( @@ -400,6 +413,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): ) -> List[BaseMessage]: from langchain_core.messages import BaseMessage + # If dictionary, try to pluck the single key representing messages if isinstance(output_val, dict): if self.output_messages_key: key = self.output_messages_key @@ -418,6 +432,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): from langchain_core.messages import AIMessage return [AIMessage(content=output_val)] + # If value is a single message, convert to a list elif isinstance(output_val, BaseMessage): return [output_val] elif isinstance(output_val, (list, tuple)): @@ -431,7 +446,10 @@ class RunnableWithMessageHistory(RunnableBindingBase): if not self.history_messages_key: # return all messages - messages += self._get_input_messages(input) + input_val = ( + input if not self.input_messages_key else input[self.input_messages_key] + ) + messages += self._get_input_messages(input_val) return messages async def _aenter_history( @@ -454,7 +472,6 @@ class RunnableWithMessageHistory(RunnableBindingBase): # Get the input messages inputs = load(run.inputs) input_messages = self._get_input_messages(inputs) - # If historic messages were prepended to the input messages, remove them to # avoid adding duplicate messages to history. if not self.history_messages_key: diff --git a/libs/core/langchain_core/tracers/base.py b/libs/core/langchain_core/tracers/base.py index c7373556c52..8f553e52f20 100644 --- a/libs/core/langchain_core/tracers/base.py +++ b/libs/core/langchain_core/tracers/base.py @@ -48,7 +48,9 @@ class BaseTracer(BaseCallbackHandler, ABC): def __init__( self, *, - _schema_format: Literal["original", "streaming_events"] = "original", + _schema_format: Literal[ + "original", "streaming_events", "original+chat" + ] = "original", **kwargs: Any, ) -> None: """Initialize the tracer. @@ -63,6 +65,8 @@ class BaseTracer(BaseCallbackHandler, ABC): for internal usage. It will likely change in the future, or be deprecated entirely in favor of a dedicated async tracer for streaming events. + - 'original+chat' is a format that is the same as 'original' + except it does NOT raise an attribute error on_chat_model_start kwargs: Additional keyword arguments that will be passed to the super class. """ @@ -163,7 +167,7 @@ class BaseTracer(BaseCallbackHandler, ABC): **kwargs: Any, ) -> Run: """Start a trace for an LLM run.""" - if self._schema_format != "streaming_events": + if self._schema_format not in ("streaming_events", "original+chat"): # Please keep this un-implemented for backwards compatibility. # When it's unimplemented old tracers that use the "original" format # fallback on the on_llm_start method implementation if they @@ -360,7 +364,7 @@ class BaseTracer(BaseCallbackHandler, ABC): def _get_chain_inputs(self, inputs: Any) -> Any: """Get the inputs for a chain run.""" - if self._schema_format == "original": + if self._schema_format in ("original", "original+chat"): return inputs if isinstance(inputs, dict) else {"input": inputs} elif self._schema_format == "streaming_events": return { @@ -371,7 +375,7 @@ class BaseTracer(BaseCallbackHandler, ABC): def _get_chain_outputs(self, outputs: Any) -> Any: """Get the outputs for a chain run.""" - if self._schema_format == "original": + if self._schema_format in ("original", "original+chat"): return outputs if isinstance(outputs, dict) else {"output": outputs} elif self._schema_format == "streaming_events": return { @@ -436,7 +440,7 @@ class BaseTracer(BaseCallbackHandler, ABC): if metadata: kwargs.update({"metadata": metadata}) - if self._schema_format == "original": + if self._schema_format in ("original", "original+chat"): inputs = {"input": input_str} elif self._schema_format == "streaming_events": inputs = {"input": inputs} diff --git a/libs/core/langchain_core/tracers/log_stream.py b/libs/core/langchain_core/tracers/log_stream.py index 8b1f2da9f59..4f156755235 100644 --- a/libs/core/langchain_core/tracers/log_stream.py +++ b/libs/core/langchain_core/tracers/log_stream.py @@ -482,7 +482,7 @@ def _get_standardized_inputs( def _get_standardized_outputs( - run: Run, schema_format: Literal["original", "streaming_events"] + run: Run, schema_format: Literal["original", "streaming_events", "original+chat"] ) -> Optional[Any]: """Extract standardized output from a run. diff --git a/libs/core/langchain_core/tracers/root_listeners.py b/libs/core/langchain_core/tracers/root_listeners.py index 7db7407d268..4b33a2fe53a 100644 --- a/libs/core/langchain_core/tracers/root_listeners.py +++ b/libs/core/langchain_core/tracers/root_listeners.py @@ -22,7 +22,7 @@ class RootListenersTracer(BaseTracer): on_end: Optional[Listener], on_error: Optional[Listener], ) -> None: - super().__init__() + super().__init__(_schema_format="original+chat") self.config = config self._arg_on_start = on_start diff --git a/libs/core/tests/unit_tests/fake/memory.py b/libs/core/tests/unit_tests/fake/memory.py index 43dd53dc5eb..d8431fa8599 100644 --- a/libs/core/tests/unit_tests/fake/memory.py +++ b/libs/core/tests/unit_tests/fake/memory.py @@ -17,6 +17,8 @@ class ChatMessageHistory(BaseChatMessageHistory, BaseModel): def add_message(self, message: BaseMessage) -> None: """Add a self-created message to the store""" + if not isinstance(message, BaseMessage): + raise ValueError self.messages.append(message) def clear(self) -> None: