mirror of
https://github.com/hwchase17/langchain.git
synced 2026-04-04 11:25:11 +00:00
676 lines
26 KiB
Plaintext
676 lines
26 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "6a4becbd-238e-4c1d-a02d-08e61fbc3763",
|
|
"metadata": {},
|
|
"source": [
|
|
"# How to add message history\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",
|
|
"\n",
|
|
"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",
|
|
"* a dict with a key that takes the latest message(s) as a string or sequence of `BaseMessages`, and a separate key that takes historical messages\n",
|
|
"\n",
|
|
"And returns as output one of\n",
|
|
"\n",
|
|
"* a string that can be treated as the contents of an `AIMessage`\n",
|
|
"* a sequence of `BaseMessage`\n",
|
|
"* a dict with a key that contains a sequence of `BaseMessage`\n",
|
|
"\n",
|
|
"```{=mdx}\n",
|
|
"import PrerequisiteLinks from \"@theme/PrerequisiteLinks\";\n",
|
|
"\n",
|
|
"<PrerequisiteLinks content={`\n",
|
|
"- [LangChain Expression Language (LCEL)](/docs/concepts/#langchain-expression-language)\n",
|
|
"- [Chaining runnables](/docs/how_to/sequence/)\n",
|
|
"- [Configuring chain parameters at runtime](/docs/how_to/configure)\n",
|
|
"- [Prompt templates](/docs/concepts/#prompt-templates)\n",
|
|
"- [Chat Messages](/docs/concepts/#message-types)\n",
|
|
"`} />\n",
|
|
"```\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",
|
|
"\n",
|
|
"```{=mdx}\n",
|
|
"import ChatModelTabs from \"@theme/ChatModelTabs\";\n",
|
|
"\n",
|
|
"<ChatModelTabs\n",
|
|
" customVarName=\"llm\"\n",
|
|
"/>\n",
|
|
"```"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"id": "6489f585",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# | output: false\n",
|
|
"# | echo: false\n",
|
|
"\n",
|
|
"%pip install -qU langchain langchain_anthropic\n",
|
|
"\n",
|
|
"import os\n",
|
|
"from getpass import getpass\n",
|
|
"\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,
|
|
"id": "2ed413b4-33a1-48ee-89b0-2d4917ec101a",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
|
|
"from langchain_openai.chat_models import ChatOpenAI\n",
|
|
"\n",
|
|
"prompt = ChatPromptTemplate.from_messages(\n",
|
|
" [\n",
|
|
" (\n",
|
|
" \"system\",\n",
|
|
" \"You're an assistant who's good at {ability}. 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",
|
|
"\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,\n",
|
|
" get_session_history,\n",
|
|
" input_messages_key=\"input\",\n",
|
|
" history_messages_key=\"history\",\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "01acb505-3fd3-4ab4-9f04-5ea07e81542e",
|
|
"metadata": {},
|
|
"source": [
|
|
"Note that we've specified `input_messages_key` (the key to be treated as the latest input message) and `history_messages_key` (the key to add historical messages to).\n",
|
|
"\n",
|
|
"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",
|
|
"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_017rAM9qrBTSdJ5i1rwhB7bT', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 32, 'output_tokens': 31}}, id='run-65e94a5e-a804-40de-ba88-d01b6cd06864-0')"
|
|
]
|
|
},
|
|
"execution_count": 4,
|
|
"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",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 5,
|
|
"id": "954688a2-9a3f-47ee-a9e8-fa0c83e69477",
|
|
"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_017hK1Q63ganeQZ9wdeqruLP', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 68, 'output_tokens': 31}}, id='run-a42177ef-b04a-4968-8606-446fb465b943-0')"
|
|
]
|
|
},
|
|
"execution_count": 5,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Remembers\n",
|
|
"with_message_history.invoke(\n",
|
|
" {\"ability\": \"math\", \"input\": \"What?\"},\n",
|
|
" config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 6,
|
|
"id": "39350d7c-2641-4744-bc2a-fd6a57c4ea90",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"AIMessage(content=\"I'm an AI assistant skilled in mathematics. How can I help you with a math-related task?\", response_metadata={'id': 'msg_01AYwfQ6SH5qz8ZQMW3nYtGU', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 28, 'output_tokens': 24}}, id='run-c57d93e3-305f-4c0e-bdb9-ef82f5b49f61-0')"
|
|
]
|
|
},
|
|
"execution_count": 6,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"# New session_id --> does not remember.\n",
|
|
"with_message_history.invoke(\n",
|
|
" {\"ability\": \"math\", \"input\": \"What?\"},\n",
|
|
" config={\"configurable\": {\"session_id\": \"def234\"}},\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "d29497be-3366-408d-bbb9-d4a8bf4ef37c",
|
|
"metadata": {},
|
|
"source": [
|
|
"The configuration parameters by which we track message histories can be customized by passing in a list of ``ConfigurableFieldSpec`` objects to the ``history_factory_config`` parameter. Below, we use two parameters: a `user_id` and `conversation_id`."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 7,
|
|
"id": "1c89daee-deff-4fdf-86a3-178f7d8ef536",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"AIMessage(content='Hello! How can I assist you with math today?', response_metadata={'id': 'msg_01UdhnwghuSE7oRM57STFhHL', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 27, 'output_tokens': 14}}, id='run-3d53f67a-4ea7-4d78-8e67-37db43d4af5d-0')"
|
|
]
|
|
},
|
|
"execution_count": 7,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"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",
|
|
"\n",
|
|
"\n",
|
|
"with_message_history = RunnableWithMessageHistory(\n",
|
|
" runnable,\n",
|
|
" get_session_history,\n",
|
|
" input_messages_key=\"input\",\n",
|
|
" history_messages_key=\"history\",\n",
|
|
" history_factory_config=[\n",
|
|
" ConfigurableFieldSpec(\n",
|
|
" id=\"user_id\",\n",
|
|
" annotation=str,\n",
|
|
" name=\"User ID\",\n",
|
|
" description=\"Unique identifier for the user.\",\n",
|
|
" default=\"\",\n",
|
|
" is_shared=True,\n",
|
|
" ),\n",
|
|
" ConfigurableFieldSpec(\n",
|
|
" id=\"conversation_id\",\n",
|
|
" annotation=str,\n",
|
|
" name=\"Conversation ID\",\n",
|
|
" description=\"Unique identifier for the conversation.\",\n",
|
|
" default=\"\",\n",
|
|
" is_shared=True,\n",
|
|
" ),\n",
|
|
" ],\n",
|
|
")\n",
|
|
"\n",
|
|
"with_message_history.invoke(\n",
|
|
" {\"ability\": \"math\", \"input\": \"Hello\"},\n",
|
|
" config={\"configurable\": {\"user_id\": \"123\", \"conversation_id\": \"1\"}},\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"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": 9,
|
|
"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 this radical freedom comes great responsibility. De Beauvoir criticized \"bad faith\" - the tendency of people to deny their freedom and responsibility by making excuses or hiding behind social roles and norms.\\n\\n4. Ethical Engagement: For de Beauvoir, true freedom and authenticity required ethical engagement with the world and with others. We must take responsibility for our choices and their impact on others.\\n\\nOverall, de Beauvoir saw free will as a core aspect of the human condition, but one that is fraught with difficulty and ambiguity. Her philosophy emphasized the importance of owning our freedom and using it to ethically shape our lives and world.', response_metadata={'id': 'msg_01A78LdxxsCm6uR8vcAdMQBt', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 20, 'output_tokens': 293}}, id='run-9447a229-5d17-4b20-a48b-7507b78b225a-0')}"
|
|
]
|
|
},
|
|
"execution_count": 9,
|
|
"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": 10,
|
|
"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, but not identical, to those of her long-time partner Jean-Paul Sartre, another prominent existentialist philosopher.\\n\\nKey similarities:\\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 biology.\\n\\n3. Emphasis on Responsibility: They agreed that with radical freedom comes great responsibility for one's choices and their consequences.\\n\\nKey differences:\\n\\n1. Ambiguity of the Human Condition: While Sartre emphasized the pure, unconditioned nature of human freedom, de Beauvoir recognized the ambiguity of the human condition - our freedom is constrained by our facticity (circumstances).\\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 on the individual's freedom.\\n\\n3. Gendered Perspectives: As a woman, de Beauvoir's perspective was more attuned to issues of gender and the lived experience of women, which shaped her views on freedom and ethics.\\n\\nSo in summary, while Sartre and de Beauvoir shared a core existentialist philosophy centered on radical human freedom, de Beauvoir's thought incorporated a greater recognition of the ambiguity and ethical dimensions of the human condition. This reflected her distinct feminist and phenomenological approach.\", response_metadata={'id': 'msg_01U6X3KNPufVg3zFvnx24eKq', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 324, 'output_tokens': 338}}, id='run-c4a984bd-33c6-4e26-a4d1-d58b666d065c-0')}"
|
|
]
|
|
},
|
|
"execution_count": 10,
|
|
"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=<anthropic.Anthropic object at 0x1077ff5b0>, _async_client=<anthropic.AsyncAnthropic object at 0x1321c71f0>), config_factories=[<function Runnable.with_listeners.<locals>.<lambda> at 0x1473dd000>]), config={'run_name': 'RunnableWithMessageHistory'}), get_session_history=<function get_session_history at 0x1374c7be0>, history_factory_config=[ConfigurableFieldSpec(id='session_id', annotation=<class 'str'>, 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=<anthropic.Anthropic object at 0x1077ff5b0>, _async_client=<anthropic.AsyncAnthropic object at 0x1321c71f0>), config_factories=[<function Runnable.with_listeners.<locals>.<lambda> at 0x1473df6d0>]), config={'run_name': 'RunnableWithMessageHistory'}), get_session_history=<function get_session_history at 0x1374c7be0>, input_messages_key='input_messages', history_factory_config=[ConfigurableFieldSpec(id='session_id', annotation=<class 'str'>, 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": null,
|
|
"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": null,
|
|
"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](/docs/langsmith)\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": null,
|
|
"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": null,
|
|
"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": null,
|
|
"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 in a right triangle.')"
|
|
]
|
|
},
|
|
"execution_count": 11,
|
|
"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": null,
|
|
"id": "ab29abd3-751f-41ce-a1b0-53f6b565e79d",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"AIMessage(content='The inverse of cosine is the arccosine function, denoted as acos or cos^-1, which gives the angle corresponding to a given cosine value.')"
|
|
]
|
|
},
|
|
"execution_count": 12,
|
|
"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": [
|
|
":::{.callout-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": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3 (ipykernel)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.10.1"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|