{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6a4becbd-238e-4c1d-a02d-08e61fbc3763",
   "metadata": {},
   "source": [
    "# Add message history (memory)\n",
    "\n",
    "The `RunnableWithMessageHistory` let's us add message history to certain types of chains.\n",
    "\n",
    "Specifically, it can be used for any Runnable that takes as input one of\n",
    "* a sequence of `BaseMessage`\n",
    "* a dict with a key that takes a sequence of `BaseMessage`\n",
    "* a dict with a key that takes the latest message(s) as a string or sequence of `BaseMessage`, and a separate key that takes historical messages\n",
    "\n",
    "And returns as output one of\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",
    "Let's take a look at some examples to see how it works."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6bca45e5-35d9-4603-9ca9-6ac0ce0e35cd",
   "metadata": {},
   "source": [
    "## Setup\n",
    "\n",
    "We'll use Redis to store our chat message histories and Anthropic's claude-2 model so we'll need to install the following dependencies:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "477d04b3-c2b6-4ba5-962f-492c0d625cd5",
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install -U langchain redis anthropic"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93776323-d6b8-4912-bb6a-867c5e655f46",
   "metadata": {},
   "source": [
    "Set your [Anthropic API  key](https://console.anthropic.com/):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c7f56f69-d2f1-4a21-990c-b5551eb012fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "import getpass\n",
    "import os\n",
    "\n",
    "os.environ[\"ANTHROPIC_API_KEY\"] = getpass.getpass()"
   ]
  },
  {
   "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": 1,
   "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": 2,
   "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": "1a5a632e-ba9e-4488-b586-640ad5494f62",
   "metadata": {},
   "source": [
    "## Example: Dict input, message output\n",
    "\n",
    "Let's create a simple chain that takes a dict as input and returns a BaseMessage.\n",
    "\n",
    "In this case the `\"question\"` key in the input represents our input message, and the `\"history\"` key is where our historical messages will be injected."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "2a150d6f-8878-4950-8634-a608c5faad56",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Optional\n",
    "\n",
    "from langchain.chat_models import ChatAnthropic\n",
    "from langchain.memory.chat_message_histories import RedisChatMessageHistory\n",
    "from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "from langchain.schema.chat_history import BaseChatMessageHistory\n",
    "from langchain.schema.runnable.history import RunnableWithMessageHistory"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "3185edba-4eb6-4b32-80c6-577c0d19af97",
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\"system\", \"You're an assistant who's good at {ability}\"),\n",
    "        MessagesPlaceholder(variable_name=\"history\"),\n",
    "        (\"human\", \"{question}\"),\n",
    "    ]\n",
    ")\n",
    "\n",
    "chain = prompt | ChatAnthropic(model=\"claude-2\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f9d81796-ce61-484c-89e2-6c567d5e54ef",
   "metadata": {},
   "source": [
    "### Adding message history\n",
    "\n",
    "To add message history to our original chain we wrap it in the `RunnableWithMessageHistory` class.\n",
    "\n",
    "Crucially, we also need to  define a method that takes a session_id string and based on it returns a `BaseChatMessageHistory`. Given the same input, this method should return an equivalent output.\n",
    "\n",
    "In this case we'll also want to specify `input_messages_key` (the key to be treated as the latest input message) and `history_messages_key` (the key to add historical messages to)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ca7c64d8-e138-4ef8-9734-f82076c47d80",
   "metadata": {},
   "outputs": [],
   "source": [
    "chain_with_history = RunnableWithMessageHistory(\n",
    "    chain,\n",
    "    lambda session_id: RedisChatMessageHistory(session_id, url=REDIS_URL),\n",
    "    input_messages_key=\"question\",\n",
    "    history_messages_key=\"history\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37eefdec-9901-4650-b64c-d3c097ed5f4d",
   "metadata": {},
   "source": [
    "## Invoking with config\n",
    "\n",
    "Whenever we call our chain with message history, we need to include a config that contains the `session_id`\n",
    "```python\n",
    "config={\"configurable\": {\"session_id\": \"<SESSION_ID>\"}}\n",
    "```\n",
    "\n",
    "Given the same configuration, our chain should be pulling from the same chat message history."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "a85bcc22-ca4c-4ad5-9440-f94be7318f3e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content=' Cosine is one of the basic trigonometric functions in mathematics. It is defined as the ratio of the adjacent side to the hypotenuse in a right triangle.\\n\\nSome key properties and facts about cosine:\\n\\n- It is denoted by cos(θ), where θ is the angle in a right triangle. \\n\\n- The cosine of an acute angle is always positive. For angles greater than 90 degrees, cosine can be negative.\\n\\n- Cosine is one of the three main trig functions along with sine and tangent.\\n\\n- The cosine of 0 degrees is 1. As the angle increases towards 90 degrees, the cosine value decreases towards 0.\\n\\n- The range of values for cosine is -1 to 1.\\n\\n- The cosine function maps angles in a circle to the x-coordinate on the unit circle.\\n\\n- Cosine is used to find adjacent side lengths in right triangles, and has many other applications in mathematics, physics, engineering and more.\\n\\n- Key cosine identities include: cos(A+B) = cosAcosB − sinAsinB and cos(2A) = cos^2(A) − sin^2(A)\\n\\nSo in summary, cosine is a fundamental trig')"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chain_with_history.invoke(\n",
    "    {\"ability\": \"math\", \"question\": \"What does cosine mean?\"},\n",
    "    config={\"configurable\": {\"session_id\": \"foobar\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "ab29abd3-751f-41ce-a1b0-53f6b565e79d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content=' The inverse of the cosine function is called the arccosine or inverse cosine, often denoted as cos-1(x) or arccos(x).\\n\\nThe key properties and facts about arccosine:\\n\\n- It is defined as the angle θ between 0 and π radians whose cosine is x. So arccos(x) = θ such that cos(θ) = x.\\n\\n- The range of arccosine is 0 to π radians (0 to 180 degrees).\\n\\n- The domain of arccosine is -1 to 1. \\n\\n- arccos(cos(θ)) = θ for values of θ from 0 to π radians.\\n\\n- arccos(x) is the angle in a right triangle whose adjacent side is x and hypotenuse is 1.\\n\\n- arccos(0) = 90 degrees. As x increases from 0 to 1, arccos(x) decreases from 90 to 0 degrees.\\n\\n- arccos(1) = 0 degrees. arccos(-1) = 180 degrees.\\n\\n- The graph of y = arccos(x) is part of the unit circle, restricted to x')"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chain_with_history.invoke(\n",
    "    {\"ability\": \"math\", \"question\": \"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/863a003b-7ca8-4b24-be9e-d63ec13c106e/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": "028cf151-6cd5-4533-b3cf-c8d735554647",
   "metadata": {},
   "source": [
    "## Example: messages input, dict output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "0bb446b5-6251-45fe-a92a-4c6171473c53",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'output_message': AIMessage(content=' Here is a summary of Simone de Beauvoir\\'s views on free will:\\n\\n- De Beauvoir was an existentialist philosopher and believed strongly in the concept of free will. She rejected the idea that human nature or instincts determine behavior.\\n\\n- Instead, de Beauvoir argued that human beings define their own essence or nature through their actions and choices. As she famously wrote, \"One is not born, but rather becomes, a woman.\"\\n\\n- De Beauvoir believed that while individuals are situated in certain cultural contexts and social conditions, they still have agency and the ability to transcend these situations. Freedom comes from choosing one\\'s attitude toward these constraints.\\n\\n- She emphasized the radical freedom and responsibility of the individual. We are \"condemned to be free\" because we cannot escape making choices and taking responsibility for our choices. \\n\\n- De Beauvoir felt that many people evade their freedom and responsibility by adopting rigid mindsets, ideologies, or conforming uncritically to social roles.\\n\\n- She advocated for the recognition of ambiguity in the human condition and warned against the quest for absolute rules that deny freedom and responsibility. Authentic living involves embracing ambiguity.\\n\\nIn summary, de Beauvoir promoted an existential ethics')}"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain.schema.messages import HumanMessage\n",
    "from langchain.schema.runnable import RunnableParallel\n",
    "\n",
    "chain = RunnableParallel({\"output_message\": ChatAnthropic(model=\"claude-2\")})\n",
    "chain_with_history = RunnableWithMessageHistory(\n",
    "    chain,\n",
    "    lambda session_id: RedisChatMessageHistory(session_id, url=REDIS_URL),\n",
    "    output_messages_key=\"output_message\",\n",
    ")\n",
    "\n",
    "chain_with_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": 16,
   "id": "601ce3ff-aea8-424d-8e54-fd614256af4f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'output_message': AIMessage(content=\" There are many similarities between Simone de Beauvoir's views on free will and those of Jean-Paul Sartre, though some key differences emerge as well:\\n\\nSimilarities with Sartre:\\n\\n- Both were existentialist thinkers who rejected determinism and emphasized human freedom and responsibility.\\n\\n- They agreed that existence precedes essence - there is no predefined human nature that determines who we are.\\n\\n- Individuals must define themselves through their choices and actions. This leads to anxiety but also freedom.\\n\\n- The human condition is characterized by ambiguity and uncertainty, rather than fixed meanings/values.\\n\\n- Both felt that most people evade their freedom through self-deception, conformity, or adopting collective identities/values uncritically.\\n\\nDifferences from Sartre: \\n\\n- Sartre placed more emphasis on the burden and anguish of radical freedom. De Beauvoir focused more on its positive potential.\\n\\n- De Beauvoir critiqued Sartre's premise that human relations are necessarily conflictual. She saw more potential for mutual recognition.\\n\\n- Sartre saw the Other's gaze as a threat to freedom. De Beauvoir put more stress on how the Other's gaze can confirm\")}"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chain_with_history.invoke(\n",
    "    [HumanMessage(content=\"How did this compare to Sartre\")],\n",
    "    config={\"configurable\": {\"session_id\": \"baz\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b898d1b1-11e6-4d30-a8dd-cc5e45533611",
   "metadata": {},
   "source": [
    ":::tip\n",
    "\n",
    "[LangSmith trace](https://smith.langchain.com/public/f6c3e1d1-a49d-4955-a9fa-c6519df74fa7/r)\n",
    "\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1724292c-01c6-44bb-83e8-9cdb6bf01483",
   "metadata": {},
   "source": [
    "## More examples\n",
    "\n",
    "We could also do any of the below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fd89240b-5a25-48f8-9568-5c1127f9ffad",
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "\n",
    "# messages in, messages out\n",
    "RunnableWithMessageHistory(\n",
    "    ChatAnthropic(model=\"claude-2\"),\n",
    "    lambda session_id: RedisChatMessageHistory(session_id, url=REDIS_URL),\n",
    ")\n",
    "\n",
    "# dict with single key for all messages in, messages out\n",
    "RunnableWithMessageHistory(\n",
    "    itemgetter(\"input_messages\") | ChatAnthropic(model=\"claude-2\"),\n",
    "    lambda session_id: RedisChatMessageHistory(session_id, url=REDIS_URL),\n",
    "    input_messages_key=\"input_messages\",\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "poetry-venv",
   "language": "python",
   "name": "poetry-venv"
  },
  "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.9.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
