From b6db631837d24ed7556ac28030cd9a032d816a91 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Fri, 23 Aug 2024 17:09:55 -0700 Subject: [PATCH] cr --- .../add-summary-conversation-history.ipynb | 254 ++++++++---------- 1 file changed, 108 insertions(+), 146 deletions(-) diff --git a/examples/how-tos/add-summary-conversation-history.ipynb b/examples/how-tos/add-summary-conversation-history.ipynb index 3238a8469..761973ee1 100644 --- a/examples/how-tos/add-summary-conversation-history.ipynb +++ b/examples/how-tos/add-summary-conversation-history.ipynb @@ -24,64 +24,24 @@ "source": [ "## Setup\n", "\n", - "First, let's set up the packages we're going to want to use" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_anthropic" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for Anthropic (the LLM we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import os\n", + "First, let's set up the packages we're going to want to use\n", "\n", + "```bash\n", + "npm install @langchain/langgraph @langchain/anthropic uuid\n", + "```\n", "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "Next, we need to set API keys for Anthropic (the LLM we will use)\n", "\n", + "```typescript\n", + "process.env.ANTHROPIC_API_KEY = 'YOUR_API_KEY'\n", + "```\n", "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability.\n", + "\n", + "```typescript\n", + "process.env.LANGCHAIN_TRACING_V2 = 'true'\n", + "process.env.LANGCHAIN_API_KEY = 'YOUR_API_KEY'\n", + "```" ] }, { @@ -96,102 +56,104 @@ }, { "cell_type": "code", - "execution_count": 25, - "id": "378899a9-3b9a-4748-95b6-eb00e0828677", + "execution_count": null, + "id": "1a76502d", "metadata": {}, "outputs": [], "source": [ - "from typing import Literal\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_core.messages import SystemMessage, RemoveMessage\n", - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import MessagesState, StateGraph, START, END\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "\n", - "\n", - "# We will add a `summary` attribute (in addition to `messages` key,\n", - "# which MessagesState already has)\n", - "class State(MessagesState):\n", - " summary: str\n", - "\n", - "\n", - "# We will use this model for both the conversation and the summarization\n", - "model = ChatAnthropic(model_name=\"claude-3-haiku-20240307\")\n", - "\n", - "\n", - "# Define the logic to call the model\n", - "def call_model(state: State):\n", - " # If a summary exists, we add this in as a system message\n", - " summary = state.get(\"summary\", \"\")\n", - " if summary:\n", - " system_message = f\"Summary of conversation earlier: {summary}\"\n", - " messages = [SystemMessage(content=system_message)] + state[\"messages\"]\n", - " else:\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# We now define the logic for determining whether to end or summarize the conversation\n", - "def should_continue(state: State) -> Literal[\"summarize_conversation\", END]:\n", - " \"\"\"Return the next node to execute.\"\"\"\n", - " messages = state[\"messages\"]\n", - " # If there are more than six messages, then we summarize the conversation\n", - " if len(messages) > 6:\n", - " return \"summarize_conversation\"\n", - " # Otherwise we can just end\n", - " return END\n", - "\n", - "\n", - "def summarize_conversation(state: State):\n", - " # First, we summarize the conversation\n", - " summary = state.get(\"summary\", \"\")\n", - " if summary:\n", - " # If a summary already exists, we use a different system prompt\n", - " # to summarize it than if one didn't\n", - " summary_message = (\n", - " f\"This is summary of the conversation to date: {summary}\\n\\n\"\n", - " \"Extend the summary by taking into account the new messages above:\"\n", - " )\n", - " else:\n", - " summary_message = \"Create a summary of the conversation above:\"\n", - "\n", - " messages = state[\"messages\"] + [HumanMessage(content=summary_message)]\n", - " response = model.invoke(messages)\n", - " # We now need to delete messages that we no longer want to show up\n", - " # I will delete all but the last two messages, but you can change this\n", - " delete_messages = [RemoveMessage(id=m.id) for m in state[\"messages\"][:-2]]\n", - " return {\"summary\": response.content, \"messages\": delete_messages}\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(State)\n", - "\n", - "# Define the conversation node and the summarize node\n", - "workflow.add_node(\"conversation\", call_model)\n", - "workflow.add_node(summarize_conversation)\n", - "\n", - "# Set the entrypoint as conversation\n", - "workflow.add_edge(START, \"conversation\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `conversation`.\n", - " # This means these are the edges taken after the `conversation` node is called.\n", + "import { ChatAnthropic } from \"@langchain/anthropic\";\n", + "import { SystemMessage, HumanMessage, AIMessage, RemoveMessage } from \"@langchain/core/messages\";\n", + "import { MemorySaver } from \"@langchain/langgraph-checkpoint\";\n", + "import { MessagesAnnotation, StateGraph, START, END, Annotation } from \"@langchain/langgraph\";\n", + "import { v4 as uuidv4 } from \"uuid\";\n", + "\n", + "const memory = new MemorySaver();\n", + "\n", + "// We will add a `summary` attribute (in addition to `messages` key,\n", + "// which MessagesState already has)\n", + "const GraphAnnotation = Annotation.Root({\n", + " ...MessagesAnnotation.spec,\n", + " summary: Annotation({\n", + " default: () => \"\",\n", + " })\n", + "})\n", + "\n", + "// We will use this model for both the conversation and the summarization\n", + "const model = new ChatAnthropic({ model: \"claude-3-haiku-20240307\" });\n", + "\n", + "// Define the logic to call the model\n", + "async function callModel(state: typeof GraphAnnotation.State) {\n", + " // If a summary exists, we add this in as a system message\n", + " const { summary } = state;\n", + " let { messages } = state;\n", + " if (summary) {\n", + " const systemMessage = new SystemMessage({\n", + " id: uuidv4(),\n", + " content: `Summary of conversation earlier: ${summary}`\n", + " });\n", + " messages = [systemMessage, ...messages];\n", + " }\n", + " const response = await model.invoke(messages);\n", + " // We return an object, because this will get added to the existing state\n", + " return { messages: [response] };\n", + "}\n", + "\n", + "// We now define the logic for determining whether to end or summarize the conversation\n", + "function shouldContinue(state: typeof GraphAnnotation.State): \"summarize_conversation\" | typeof END {\n", + " const messages = state.messages;\n", + " // If there are more than six messages, then we summarize the conversation\n", + " if (messages.length > 6) {\n", + " return \"summarize_conversation\";\n", + " }\n", + " // Otherwise we can just end\n", + " return END;\n", + "}\n", + "\n", + "async function summarizeConversation(state: typeof GraphAnnotation.State) {\n", + " // First, we summarize the conversation\n", + " const { summary, messages } = state;\n", + " let summaryMessage: string;\n", + " if (summary) {\n", + " // If a summary already exists, we use a different system prompt\n", + " // to summarize it than if one didn't\n", + " summaryMessage = `This is summary of the conversation to date: ${summary}\\n\\n` +\n", + " \"Extend the summary by taking into account the new messages above:\";\n", + " } else {\n", + " summaryMessage = \"Create a summary of the conversation above:\";\n", + " }\n", + "\n", + " const allMessages = [...messages, new HumanMessage({\n", + " id: uuidv4(),\n", + " content: summaryMessage,\n", + " })];\n", + " const response = await model.invoke(allMessages);\n", + " // We now need to delete messages that we no longer want to show up\n", + " // I will delete all but the last two messages, but you can change this\n", + " const deleteMessages = messages.slice(0, -2).map((m) => new RemoveMessage(m.id));\n", + " return { summary: response.content, messages: deleteMessages };\n", + "}\n", + "\n", + "// Define a new graph\n", + "const workflow = new StateGraph(GraphAnnotation)\n", + " // Define the conversation node and the summarize node\n", + " .addNode(\"conversation\", callModel)\n", + " .addNode(\"summarize_conversation\", summarizeConversation)\n", + " // Set the entrypoint as conversation\n", + " .addEdge(START, \"conversation\")\n", + " // We now add a conditional edge\n", + " .addConditionalEdges(\n", + " // First, we define the start node. We use `conversation`.\n", + " // This means these are the edges taken after the `conversation` node is called.\n", " \"conversation\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - ")\n", - "\n", - "# We now add a normal edge from `summarize_conversation` to END.\n", - "# This means that after `summarize_conversation` is called, we end.\n", - "workflow.add_edge(\"summarize_conversation\", END)\n", - "\n", - "# Finally, we compile it!\n", - "app = workflow.compile(checkpointer=memory)" + " // Next, we pass in the function that will determine which node is called next.\n", + " shouldContinue\n", + " )\n", + " // We now add a normal edge from `summarize_conversation` to END.\n", + " // This means that after `summarize_conversation` is called, we end.\n", + " .addEdge(\"summarize_conversation\", END);\n", + "\n", + "// Finally, we compile it!\n", + "const app = workflow.compile({ checkpointer: memory });" ] }, {