From 70b003219163654f5cac7723c21b61f7a5aa0595 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Mon, 30 Sep 2024 22:08:27 -0400 Subject: [PATCH] format docs --- docs/docs/how-tos/async.ipynb | 10 +- docs/docs/how-tos/configuration.ipynb | 3 + .../how-tos/create-react-agent-hitl.ipynb | 7 +- docs/docs/how-tos/disable-streaming.ipynb | 12 +- .../dynamic_breakpoints.ipynb | 9 +- .../human_in_the_loop/time-travel.ipynb | 1183 +++++++++-------- .../human_in_the_loop/wait-user-input.ipynb | 2 + docs/docs/how-tos/input_output_schema.ipynb | 2 + .../docs/how-tos/memory/delete-messages.ipynb | 2 +- .../memory/manage-conversation-history.ipynb | 4 +- docs/docs/how-tos/pass-config-to-tools.ipynb | 6 +- .../pass-run-time-values-to-tools.ipynb | 5 +- docs/docs/how-tos/persistence.ipynb | 1163 ++++++++-------- docs/docs/how-tos/persistence_redis.ipynb | 4 +- .../how-tos/react-agent-from-scratch.ipynb | 22 +- .../react-agent-structured-output.ipynb | 54 +- docs/docs/how-tos/recursion-limit.ipynb | 4 +- .../return-when-recursion-limit-hits.ipynb | 50 +- docs/docs/how-tos/run-id-langsmith.ipynb | 4 +- docs/docs/how-tos/state-model.ipynb | 1 + docs/docs/how-tos/streaming-content.ipynb | 12 +- .../streaming-events-from-within-tools.ipynb | 13 +- .../how-tos/streaming-from-final-node.ipynb | 10 +- docs/docs/how-tos/streaming-subgraphs.ipynb | 16 +- .../streaming-tokens-without-langchain.ipynb | 6 +- docs/docs/how-tos/streaming-tokens.ipynb | 4 +- .../how-tos/subgraph-transform-state.ipynb | 6 +- docs/docs/how-tos/subgraph.ipynb | 15 +- .../docs/how-tos/subgraphs-manage-state.ipynb | 109 +- docs/docs/how-tos/tool-calling-errors.ipynb | 20 +- docs/docs/how-tos/tool-calling.ipynb | 6 +- docs/docs/how-tos/visualization.ipynb | 4 +- .../agent-simulation-evaluation.ipynb | 2 +- .../information-gather-prompting.ipynb | 4 +- .../langgraph_code_assistant.ipynb | 12 +- .../customer-support/customer-support.ipynb | 36 +- docs/docs/tutorials/extraction/retries.ipynb | 6 +- docs/docs/tutorials/introduction.ipynb | 2 + docs/docs/tutorials/lats/lats.ipynb | 62 +- .../tutorials/llm-compiler/LLMCompiler.ipynb | 73 +- .../memory/long_term_memory_agent.ipynb | 47 +- .../multi_agent/agent_supervisor.ipynb | 14 +- .../hierarchical_agent_teams.ipynb | 14 +- .../plan-and-execute/plan-and-execute.ipynb | 3 +- .../rag/langgraph_adaptive_rag.ipynb | 1 - .../rag/langgraph_adaptive_rag_local.ipynb | 194 ++- .../tutorials/rag/langgraph_crag_local.ipynb | 4 +- .../tutorials/reflection/reflection.ipynb | 28 +- docs/docs/tutorials/reflexion/reflexion.ipynb | 41 +- docs/docs/tutorials/rewoo/rewoo.ipynb | 6 +- docs/docs/tutorials/storm/storm.ipynb | 4 +- docs/docs/tutorials/tnt-llm/tnt-llm.ipynb | 4 +- 52 files changed, 1821 insertions(+), 1504 deletions(-) diff --git a/docs/docs/how-tos/async.ipynb b/docs/docs/how-tos/async.ipynb index 9ebb2b25e..bd4b1c9df 100644 --- a/docs/docs/how-tos/async.ipynb +++ b/docs/docs/how-tos/async.ipynb @@ -533,12 +533,12 @@ " # because we chose to only include LLMs, these are LLM tokens\n", " try:\n", " content = op[\"value\"].content[0]\n", - " if 'partial_json' in content:\n", - " print(content['partial_json'], end=\"|\")\n", - " elif 'text' in content:\n", - " print(content['text'], end='|')\n", + " if \"partial_json\" in content:\n", + " print(content[\"partial_json\"], end=\"|\")\n", + " elif \"text\" in content:\n", + " print(content[\"text\"], end=\"|\")\n", " else:\n", - " print(content,end=\"|\")\n", + " print(content, end=\"|\")\n", " except:\n", " pass" ] diff --git a/docs/docs/how-tos/configuration.ipynb b/docs/docs/how-tos/configuration.ipynb index 62d8fecc3..a30b3cb65 100644 --- a/docs/docs/how-tos/configuration.ipynb +++ b/docs/docs/how-tos/configuration.ipynb @@ -158,6 +158,7 @@ " \"openai\": openai_model,\n", "}\n", "\n", + "\n", "def _call_model(state: AgentState, config: RunnableConfig):\n", " # Access the config through the configurable key\n", " model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n", @@ -253,12 +254,14 @@ "source": [ "from langchain_core.messages import SystemMessage\n", "\n", + "\n", "# We can define a config schema to specify the configuration options for the graph\n", "# A config schema is useful for indicating which fields are available in the configurable dict inside the config\n", "class ConfigSchema(TypedDict):\n", " model: Optional[str]\n", " system_message: Optional[str]\n", "\n", + "\n", "def _call_model(state: AgentState, config: RunnableConfig):\n", " # Access the config through the configurable key\n", " model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n", diff --git a/docs/docs/how-tos/create-react-agent-hitl.ipynb b/docs/docs/how-tos/create-react-agent-hitl.ipynb index d35cced64..aa06c11aa 100644 --- a/docs/docs/how-tos/create-react-agent-hitl.ipynb +++ b/docs/docs/how-tos/create-react-agent-hitl.ipynb @@ -176,6 +176,7 @@ ], "source": [ "from langchain_core.messages import HumanMessage\n", + "\n", "config = {\"configurable\": {\"thread_id\": \"42\"}}\n", "inputs = {\"messages\": [(\"user\", \"what is the weather in SF, CA?\")]}\n", "\n", @@ -285,10 +286,10 @@ "source": [ "state = graph.get_state(config)\n", "\n", - "last_message = state.values['messages'][-1]\n", - "last_message.tool_calls[0]['args'] = {\"location\": \"San Francisco\"}\n", + "last_message = state.values[\"messages\"][-1]\n", + "last_message.tool_calls[0][\"args\"] = {\"location\": \"San Francisco\"}\n", "\n", - "graph.update_state(config, {\"messages\": [ last_message]})" + "graph.update_state(config, {\"messages\": [last_message]})" ] }, { diff --git a/docs/docs/how-tos/disable-streaming.ipynb b/docs/docs/how-tos/disable-streaming.ipynb index 49d7c1f1d..d6fc79321 100644 --- a/docs/docs/how-tos/disable-streaming.ipynb +++ b/docs/docs/how-tos/disable-streaming.ipynb @@ -21,13 +21,15 @@ "from langgraph.graph import MessagesState\n", "from langgraph.graph import StateGraph, START, END\n", "\n", - "llm = ChatOpenAI(model=\"o1-preview\",temperature=1)\n", + "llm = ChatOpenAI(model=\"o1-preview\", temperature=1)\n", "\n", "graph_builder = StateGraph(MessagesState)\n", "\n", + "\n", "def chatbot(state: MessagesState):\n", " return {\"messages\": [llm.invoke(state[\"messages\"])]}\n", "\n", + "\n", "graph_builder.add_node(\"chatbot\", chatbot)\n", "graph_builder.add_edge(START, \"chatbot\")\n", "graph_builder.add_edge(\"chatbot\", END)\n", @@ -112,7 +114,7 @@ } ], "source": [ - "input = {\"messages\": {\"role\":\"user\", \"content\":\"how many r's are in strawberry?\"}}\n", + "input = {\"messages\": {\"role\": \"user\", \"content\": \"how many r's are in strawberry?\"}}\n", "try:\n", " async for event in graph.astream_events(input, version=\"v2\"):\n", " if event[\"event\"] == \"on_chat_model_end\":\n", @@ -138,13 +140,15 @@ "metadata": {}, "outputs": [], "source": [ - "llm = ChatOpenAI(model=\"o1-preview\",temperature=1,disable_streaming=True)\n", + "llm = ChatOpenAI(model=\"o1-preview\", temperature=1, disable_streaming=True)\n", "\n", "graph_builder = StateGraph(MessagesState)\n", "\n", + "\n", "def chatbot(state: MessagesState):\n", " return {\"messages\": [llm.invoke(state[\"messages\"])]}\n", "\n", + "\n", "graph_builder.add_node(\"chatbot\", chatbot)\n", "graph_builder.add_edge(START, \"chatbot\")\n", "graph_builder.add_edge(\"chatbot\", END)\n", @@ -187,7 +191,7 @@ } ], "source": [ - "input = {\"messages\": {\"role\":\"user\", \"content\":\"how many r's are in strawberry?\"}}\n", + "input = {\"messages\": {\"role\": \"user\", \"content\": \"how many r's are in strawberry?\"}}\n", "async for event in graph.astream_events(input, version=\"v2\"):\n", " if event[\"event\"] == \"on_chat_model_end\":\n", " print(event[\"data\"][\"output\"].content, end=\"\", flush=True)" diff --git a/docs/docs/how-tos/human_in_the_loop/dynamic_breakpoints.ipynb b/docs/docs/how-tos/human_in_the_loop/dynamic_breakpoints.ipynb index f76caf26b..e94a05c7c 100644 --- a/docs/docs/how-tos/human_in_the_loop/dynamic_breakpoints.ipynb +++ b/docs/docs/how-tos/human_in_the_loop/dynamic_breakpoints.ipynb @@ -94,12 +94,15 @@ "def step_2(state: State) -> State:\n", " # Let's optionally raise a NodeInterrupt\n", " # if the length of the input is longer than 5 characters\n", - " if len(state['input']) > 5:\n", - " raise NodeInterrupt(f\"Received input that is longer than 5 characters: {state['input']}\")\n", - " \n", + " if len(state[\"input\"]) > 5:\n", + " raise NodeInterrupt(\n", + " f\"Received input that is longer than 5 characters: {state['input']}\"\n", + " )\n", + "\n", " print(\"---Step 2---\")\n", " return state\n", "\n", + "\n", "def step_3(state: State) -> State:\n", " print(\"---Step 3---\")\n", " return state\n", diff --git a/docs/docs/how-tos/human_in_the_loop/time-travel.ipynb b/docs/docs/how-tos/human_in_the_loop/time-travel.ipynb index 7fc2ffdd6..3d12c0a74 100644 --- a/docs/docs/how-tos/human_in_the_loop/time-travel.ipynb +++ b/docs/docs/how-tos/human_in_the_loop/time-travel.ipynb @@ -1,592 +1,597 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# How to view and update past graph state\n", - "\n", - "Once you start [checkpointing](../../persistence) your graphs, you can easily **get** or **update** the state of the agent at any point in time. This permits a few things:\n", - "\n", - "1. You can surface a state during an interrupt to a user to let them accept an action.\n", - "2. You can **rewind** the graph to reproduce or avoid issues.\n", - "3. You can **modify** the state to embed your agent into a larger system, or to let the user better control its actions.\n", - "\n", - "The key methods used for this functionality are:\n", - "\n", - "- [get_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.get_state): fetch the values from the target config\n", - "- [update_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.update_state): apply the given values to the target state\n", - "\n", - "**Note:** this requires passing in a checkpointer.\n", - "\n", - "Below is a quick example." - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_openai" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for OpenAI (the LLM we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ANTHROPIC_API_KEY: ········\n" - ] - } - ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "
\n", - "

Set up LangSmith for LangGraph development

\n", - "

\n", - " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here. \n", - "

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "e36f89e5", - "metadata": {}, - "source": [ - "## Build the agent\n", - "\n", - "We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and fake tools (just for demo purposes)." - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "id": "f5319e01", - "metadata": {}, - "outputs": [], - "source": [ - "# Set up the tool\n", - "from langchain_openai import ChatOpenAI\n", - "from langchain_core.tools import tool\n", - "from langgraph.graph import MessagesState, START\n", - "from langgraph.prebuilt import ToolNode\n", - "from langgraph.graph import END, StateGraph\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "\n", - "@tool\n", - "def play_song_on_spotify(song: str):\n", - " \"\"\"Play a song on Spotify\"\"\"\n", - " # Call the spotify API ...\n", - " return f\"Successfully played {song} on Spotify!\"\n", - "\n", - "@tool\n", - "def play_song_on_apple(song: str):\n", - " \"\"\"Play a song on Apple Music\"\"\"\n", - " # Call the apple music API ...\n", - " return f\"Successfully played {song} on Apple Music!\"\n", - "\n", - "tools = [play_song_on_apple,play_song_on_spotify]\n", - "tool_node = ToolNode(tools)\n", - "\n", - "# Set up the model\n", - "\n", - "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", - "model = model.bind_tools(tools, parallel_tool_calls=False)\n", - "\n", - "\n", - "# Define nodes and conditional edges\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\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", - "# Define a new graph\n", - "workflow = StateGraph(MessagesState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.add_edge(START, \"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Set up memory\n", - "memory = MemorySaver()\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "\n", - "# We add in `interrupt_before=[\"action\"]`\n", - "# This will add a breakpoint before the `action` node is called\n", - "app = workflow.compile(checkpointer=memory)" - ] - }, - { - "cell_type": "markdown", - "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", - "metadata": {}, - "source": [ - "## Interacting with the Agent\n", - "\n", - "We can now interact with the agent. Let's ask it to play Taylor Swift's most popular song:\n" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "cfd140f0-a5a6-4697-8115-322242f197b5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "Can you play Taylor Swift's most popular song?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "Tool Calls:\n", - " play_song_on_apple (call_uhGY6Fv6Mr4ZOhSokintuoD7)\n", - " Call ID: call_uhGY6Fv6Mr4ZOhSokintuoD7\n", - " Args:\n", - " song: Anti-Hero by Taylor Swift\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: play_song_on_apple\n", - "\n", - "Succesfully played Anti-Hero by Taylor Swift on Apple Music!\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "I've successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!\n" - ] - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "input_message = HumanMessage(content=\"Can you play Taylor Swift's most popular song?\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "1c38c505-6cee-427f-9dcd-493a2ade7ebb", - "metadata": {}, - "source": [ - "## Checking history\n", - "\n", - "Let's browse the history of this thread, from start to finish." - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "id": "777538a5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", - " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}),\n", - " ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7'),\n", - " AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "app.get_state(config).values['messages']" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "8578a66d-6489-4e03-8c23-fd0530278455", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}), ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7'), AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-364f-6228-8003-dd67a426334e'}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}}, 'step': 3, 'parents': {}}, created_at='2024-09-05T21:37:39.955948+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-318f-6dc8-8002-dbdf9aaeac83'}}, tasks=())\n", - "--\n", - "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}), ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-318f-6dc8-8002-dbdf9aaeac83'}}, metadata={'source': 'loop', 'writes': {'action': {'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}}, 'step': 2, 'parents': {}}, created_at='2024-09-05T21:37:39.458185+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-3185-663e-8001-12b1ec3114b8'}}, tasks=(PregelTask(id='3a4c5ddb-14b2-5def-a766-02ddc32948ba', name='agent', error=None, interrupts=(), state=None),))\n", - "--\n", - "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}, next=('action',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-3185-663e-8001-12b1ec3114b8'}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}}, 'step': 1, 'parents': {}}, created_at='2024-09-05T21:37:39.453898+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b8-6370-8000-f9f6e7ca1b06'}}, tasks=(PregelTask(id='01f1dc72-5a39-5876-97a6-abdc12f70c2a', name='action', error=None, interrupts=(), state=None),))\n", - "--\n", - "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b8-6370-8000-f9f6e7ca1b06'}}, metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}}, created_at='2024-09-05T21:37:38.635849+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b3-6514-bfff-fe07fb36f14f'}}, tasks=(PregelTask(id='348e1ba7-95c6-5b89-80c9-1fc4720e35ef', name='agent', error=None, interrupts=(), state=None),))\n", - "--\n", - "StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b3-6514-bfff-fe07fb36f14f'}}, metadata={'source': 'input', 'writes': {'__start__': {'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\")]}}, 'step': -1, 'parents': {}}, created_at='2024-09-05T21:37:38.633849+00:00', parent_config=None, tasks=(PregelTask(id='f1cfbb8c-7792-5cf9-9d28-ae3ac7724cf3', name='__start__', error=None, interrupts=(), state=None),))\n", - "--\n" - ] - } - ], - "source": [ - "all_states = []\n", - "for state in app.get_state_history(config):\n", - " print(state)\n", - " all_states.append(state)\n", - " print(\"--\")" - ] - }, - { - "cell_type": "markdown", - "id": "0ec41c37-7c09-4cc7-8475-bf373fe66584", - "metadata": {}, - "source": [ - "## Replay a state\n", - "\n", - "We can go back to any of these states and restart the agent from there! Let's go back to right before the tool call gets executed." - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "02250602-8c4a-4fb5-bd6c-d0b9046e8699", - "metadata": {}, - "outputs": [], - "source": [ - "to_replay = all_states[2]" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "21e7fc18-6fd9-4e11-a84b-e0325c9640c8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", - " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}" - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "to_replay.values" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "id": "d4b01634-0041-4632-8d1f-5464580e54f5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('action',)" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "to_replay.next" - ] - }, - { - "cell_type": "markdown", - "id": "29da43ea-9295-43e2-b164-0eb28d96749c", - "metadata": {}, - "source": [ - "To replay from this place we just need to pass its config back to the agent. Notice that it just resumes from right where it left all - making a tool call." - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "id": "e986f94f-706f-4b6f-b3c4-f95483b9e9b8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}\n", - "{'messages': [AIMessage(content='I\\'ve started playing \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-dc338bbd-d623-40bb-b824-5d2307954b57-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}\n" - ] - } - ], - "source": [ - "for event in app.stream(None, to_replay.config):\n", - " for v in event.values():\n", - " print(v)" - ] - }, - { - "cell_type": "markdown", - "id": "59910951-fae1-4475-8511-f622439b590d", - "metadata": {}, - "source": [ - "## Branch off a past state\n", - "\n", - "Using LangGraph's checkpointing, you can do more than just replay past states. You can branch off previous locations to let the agent explore alternate trajectories or to let a user \"version control\" changes in a workflow.\n", - "\n", - "Let's show how to do this to edit the state at a particular point in time. Let's update the state to instead of playing the song on Apple to play it on Spotify:" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "id": "fbd5ad3b-5363-4ab7-ac63-b04668bc998f", - "metadata": {}, - "outputs": [], - "source": [ - "# Let's now get the last message in the state\n", - "# This is the one with the tool calls that we want to update\n", - "last_message = to_replay.values[\"messages\"][-1]\n", - "\n", - "\n", - "# Let's now update the tool we are calling\n", - "last_message.tool_calls[0]['name'] = 'play_song_on_spotify'\n", - "\n", - "branch_config = app.update_state(\n", - " to_replay.config,\n", - " {\"messages\": [last_message]},\n", - ")\n" - ] - }, - { - "cell_type": "markdown", - "id": "bced65eb-2158-43e6-a9e3-3b047c8d418e", - "metadata": {}, - "source": [ - "We can then invoke with this new `branch_config` to resume running from here with changed state. We can see from the log that the tool was called with different input." - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "9a92d3da-62e2-45a2-8545-e4f6a64e0ffe", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Spotify!', name='play_song_on_spotify', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}\n", - "{'messages': [AIMessage(content='I\\'ve started playing \"Anti-Hero\" by Taylor Swift on Spotify. Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 125, 'total_tokens': 144}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-7d8d5094-7029-4da3-9e0e-ef9d18b63615-0', usage_metadata={'input_tokens': 125, 'output_tokens': 19, 'total_tokens': 144})]}\n" - ] - } - ], - "source": [ - "for event in app.stream(None, branch_config):\n", - " for v in event.values():\n", - " print(v)" - ] - }, - { - "cell_type": "markdown", - "id": "511e319e-d10d-4b04-a4e0-fc4f3d87cb23", - "metadata": {}, - "source": [ - "Alternatively, we could update the state to not even call a tool!" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "id": "01abb480-df55-4eba-a2be-cf9372b60b54", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.messages import AIMessage\n", - "\n", - "# Let's now get the last message in the state\n", - "# This is the one with the tool calls that we want to update\n", - "last_message = to_replay.values[\"messages\"][-1]\n", - "\n", - "# Let's now get the ID for the last message, and create a new message with that ID.\n", - "new_message = AIMessage(content=\"It's quiet hours so I can't play any music right now!\", id=last_message.id)\n", - "\n", - "branch_config = app.update_state(\n", - " to_replay.config,\n", - " {\"messages\": [new_message]},\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641", - "metadata": {}, - "outputs": [], - "source": [ - "branch_state = app.get_state(branch_config)" - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "5198f9c1-d2d4-458a-993d-3caa55810b1e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", - " AIMessage(content=\"It's quiet hours so I can't play any music right now!\", id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0')]}" - ] - }, - "execution_count": 56, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "branch_state.values" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "id": "5d89d55d-db84-4c2d-828b-64a29a69947b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "()" - ] - }, - "execution_count": 57, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "branch_state.next" - ] - }, - { - "cell_type": "markdown", - "id": "cc168c90-a374-4280-a9a6-8bc232dbb006", - "metadata": {}, - "source": [ - "You can see the snapshot was updated and now correctly reflects that there is no next step." - ] - } - ], - "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.11.9" - } + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# How to view and update past graph state\n", + "\n", + "Once you start [checkpointing](../../persistence) your graphs, you can easily **get** or **update** the state of the agent at any point in time. This permits a few things:\n", + "\n", + "1. You can surface a state during an interrupt to a user to let them accept an action.\n", + "2. You can **rewind** the graph to reproduce or avoid issues.\n", + "3. You can **modify** the state to embed your agent into a larger system, or to let the user better control its actions.\n", + "\n", + "The key methods used for this functionality are:\n", + "\n", + "- [get_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.get_state): fetch the values from the target config\n", + "- [update_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.update_state): apply the given values to the target state\n", + "\n", + "**Note:** this requires passing in a checkpointer.\n", + "\n", + "Below is a quick example." + ] }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_openai" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ANTHROPIC_API_KEY: ········\n" + ] + } + ], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "
\n", + "

Set up LangSmith for LangGraph development

\n", + "

\n", + " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here. \n", + "

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "e36f89e5", + "metadata": {}, + "source": [ + "## Build the agent\n", + "\n", + "We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and fake tools (just for demo purposes)." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "f5319e01", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the tool\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.tools import tool\n", + "from langgraph.graph import MessagesState, START\n", + "from langgraph.prebuilt import ToolNode\n", + "from langgraph.graph import END, StateGraph\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "\n", + "@tool\n", + "def play_song_on_spotify(song: str):\n", + " \"\"\"Play a song on Spotify\"\"\"\n", + " # Call the spotify API ...\n", + " return f\"Successfully played {song} on Spotify!\"\n", + "\n", + "\n", + "@tool\n", + "def play_song_on_apple(song: str):\n", + " \"\"\"Play a song on Apple Music\"\"\"\n", + " # Call the apple music API ...\n", + " return f\"Successfully played {song} on Apple Music!\"\n", + "\n", + "\n", + "tools = [play_song_on_apple, play_song_on_spotify]\n", + "tool_node = ToolNode(tools)\n", + "\n", + "# Set up the model\n", + "\n", + "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", + "model = model.bind_tools(tools, parallel_tool_calls=False)\n", + "\n", + "\n", + "# Define nodes and conditional edges\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\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", + "# Define a new graph\n", + "workflow = StateGraph(MessagesState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", tool_node)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# Set up memory\n", + "memory = MemorySaver()\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "\n", + "# We add in `interrupt_before=[\"action\"]`\n", + "# This will add a breakpoint before the `action` node is called\n", + "app = workflow.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "## Interacting with the Agent\n", + "\n", + "We can now interact with the agent. Let's ask it to play Taylor Swift's most popular song:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Can you play Taylor Swift's most popular song?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "Tool Calls:\n", + " play_song_on_apple (call_uhGY6Fv6Mr4ZOhSokintuoD7)\n", + " Call ID: call_uhGY6Fv6Mr4ZOhSokintuoD7\n", + " Args:\n", + " song: Anti-Hero by Taylor Swift\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: play_song_on_apple\n", + "\n", + "Succesfully played Anti-Hero by Taylor Swift on Apple Music!\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "I've successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "input_message = HumanMessage(content=\"Can you play Taylor Swift's most popular song?\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "1c38c505-6cee-427f-9dcd-493a2ade7ebb", + "metadata": {}, + "source": [ + "## Checking history\n", + "\n", + "Let's browse the history of this thread, from start to finish." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "777538a5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}),\n", + " ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7'),\n", + " AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "app.get_state(config).values[\"messages\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "8578a66d-6489-4e03-8c23-fd0530278455", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}), ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7'), AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-364f-6228-8003-dd67a426334e'}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}}, 'step': 3, 'parents': {}}, created_at='2024-09-05T21:37:39.955948+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-318f-6dc8-8002-dbdf9aaeac83'}}, tasks=())\n", + "--\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}), ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-318f-6dc8-8002-dbdf9aaeac83'}}, metadata={'source': 'loop', 'writes': {'action': {'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}}, 'step': 2, 'parents': {}}, created_at='2024-09-05T21:37:39.458185+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-3185-663e-8001-12b1ec3114b8'}}, tasks=(PregelTask(id='3a4c5ddb-14b2-5def-a766-02ddc32948ba', name='agent', error=None, interrupts=(), state=None),))\n", + "--\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}, next=('action',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-3185-663e-8001-12b1ec3114b8'}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}}, 'step': 1, 'parents': {}}, created_at='2024-09-05T21:37:39.453898+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b8-6370-8000-f9f6e7ca1b06'}}, tasks=(PregelTask(id='01f1dc72-5a39-5876-97a6-abdc12f70c2a', name='action', error=None, interrupts=(), state=None),))\n", + "--\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b8-6370-8000-f9f6e7ca1b06'}}, metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}}, created_at='2024-09-05T21:37:38.635849+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b3-6514-bfff-fe07fb36f14f'}}, tasks=(PregelTask(id='348e1ba7-95c6-5b89-80c9-1fc4720e35ef', name='agent', error=None, interrupts=(), state=None),))\n", + "--\n", + "StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b3-6514-bfff-fe07fb36f14f'}}, metadata={'source': 'input', 'writes': {'__start__': {'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\")]}}, 'step': -1, 'parents': {}}, created_at='2024-09-05T21:37:38.633849+00:00', parent_config=None, tasks=(PregelTask(id='f1cfbb8c-7792-5cf9-9d28-ae3ac7724cf3', name='__start__', error=None, interrupts=(), state=None),))\n", + "--\n" + ] + } + ], + "source": [ + "all_states = []\n", + "for state in app.get_state_history(config):\n", + " print(state)\n", + " all_states.append(state)\n", + " print(\"--\")" + ] + }, + { + "cell_type": "markdown", + "id": "0ec41c37-7c09-4cc7-8475-bf373fe66584", + "metadata": {}, + "source": [ + "## Replay a state\n", + "\n", + "We can go back to any of these states and restart the agent from there! Let's go back to right before the tool call gets executed." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "02250602-8c4a-4fb5-bd6c-d0b9046e8699", + "metadata": {}, + "outputs": [], + "source": [ + "to_replay = all_states[2]" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "21e7fc18-6fd9-4e11-a84b-e0325c9640c8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_replay.values" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "d4b01634-0041-4632-8d1f-5464580e54f5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('action',)" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_replay.next" + ] + }, + { + "cell_type": "markdown", + "id": "29da43ea-9295-43e2-b164-0eb28d96749c", + "metadata": {}, + "source": [ + "To replay from this place we just need to pass its config back to the agent. Notice that it just resumes from right where it left all - making a tool call." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "e986f94f-706f-4b6f-b3c4-f95483b9e9b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}\n", + "{'messages': [AIMessage(content='I\\'ve started playing \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-dc338bbd-d623-40bb-b824-5d2307954b57-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}\n" + ] + } + ], + "source": [ + "for event in app.stream(None, to_replay.config):\n", + " for v in event.values():\n", + " print(v)" + ] + }, + { + "cell_type": "markdown", + "id": "59910951-fae1-4475-8511-f622439b590d", + "metadata": {}, + "source": [ + "## Branch off a past state\n", + "\n", + "Using LangGraph's checkpointing, you can do more than just replay past states. You can branch off previous locations to let the agent explore alternate trajectories or to let a user \"version control\" changes in a workflow.\n", + "\n", + "Let's show how to do this to edit the state at a particular point in time. Let's update the state to instead of playing the song on Apple to play it on Spotify:" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "fbd5ad3b-5363-4ab7-ac63-b04668bc998f", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's now get the last message in the state\n", + "# This is the one with the tool calls that we want to update\n", + "last_message = to_replay.values[\"messages\"][-1]\n", + "\n", + "\n", + "# Let's now update the tool we are calling\n", + "last_message.tool_calls[0][\"name\"] = \"play_song_on_spotify\"\n", + "\n", + "branch_config = app.update_state(\n", + " to_replay.config,\n", + " {\"messages\": [last_message]},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "bced65eb-2158-43e6-a9e3-3b047c8d418e", + "metadata": {}, + "source": [ + "We can then invoke with this new `branch_config` to resume running from here with changed state. We can see from the log that the tool was called with different input." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "9a92d3da-62e2-45a2-8545-e4f6a64e0ffe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Spotify!', name='play_song_on_spotify', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}\n", + "{'messages': [AIMessage(content='I\\'ve started playing \"Anti-Hero\" by Taylor Swift on Spotify. Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 125, 'total_tokens': 144}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-7d8d5094-7029-4da3-9e0e-ef9d18b63615-0', usage_metadata={'input_tokens': 125, 'output_tokens': 19, 'total_tokens': 144})]}\n" + ] + } + ], + "source": [ + "for event in app.stream(None, branch_config):\n", + " for v in event.values():\n", + " print(v)" + ] + }, + { + "cell_type": "markdown", + "id": "511e319e-d10d-4b04-a4e0-fc4f3d87cb23", + "metadata": {}, + "source": [ + "Alternatively, we could update the state to not even call a tool!" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "01abb480-df55-4eba-a2be-cf9372b60b54", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.messages import AIMessage\n", + "\n", + "# Let's now get the last message in the state\n", + "# This is the one with the tool calls that we want to update\n", + "last_message = to_replay.values[\"messages\"][-1]\n", + "\n", + "# Let's now get the ID for the last message, and create a new message with that ID.\n", + "new_message = AIMessage(\n", + " content=\"It's quiet hours so I can't play any music right now!\", id=last_message.id\n", + ")\n", + "\n", + "branch_config = app.update_state(\n", + " to_replay.config,\n", + " {\"messages\": [new_message]},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641", + "metadata": {}, + "outputs": [], + "source": [ + "branch_state = app.get_state(branch_config)" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "5198f9c1-d2d4-458a-993d-3caa55810b1e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", + " AIMessage(content=\"It's quiet hours so I can't play any music right now!\", id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0')]}" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "branch_state.values" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "5d89d55d-db84-4c2d-828b-64a29a69947b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "branch_state.next" + ] + }, + { + "cell_type": "markdown", + "id": "cc168c90-a374-4280-a9a6-8bc232dbb006", + "metadata": {}, + "source": [ + "You can see the snapshot was updated and now correctly reflects that there is no next step." + ] + } + ], + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/docs/docs/how-tos/human_in_the_loop/wait-user-input.ipynb b/docs/docs/how-tos/human_in_the_loop/wait-user-input.ipynb index a3b4c36fb..f773e1b92 100644 --- a/docs/docs/how-tos/human_in_the_loop/wait-user-input.ipynb +++ b/docs/docs/how-tos/human_in_the_loop/wait-user-input.ipynb @@ -382,6 +382,7 @@ "\n", "from pydantic import BaseModel\n", "\n", + "\n", "# We are going \"bind\" all tools to the model\n", "# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n", "# Since `bind_tools` takes in tools but also just tool definitions,\n", @@ -396,6 +397,7 @@ "\n", "# Define nodes and conditional edges\n", "\n", + "\n", "# Define the function that determines whether to continue or not\n", "def should_continue(state):\n", " messages = state[\"messages\"]\n", diff --git a/docs/docs/how-tos/input_output_schema.ipynb b/docs/docs/how-tos/input_output_schema.ipynb index d902d64c8..7d17a3743 100644 --- a/docs/docs/how-tos/input_output_schema.ipynb +++ b/docs/docs/how-tos/input_output_schema.ipynb @@ -79,9 +79,11 @@ "class OutputState(TypedDict):\n", " answer: str\n", "\n", + "\n", "class OverallState(InputState, OutputState):\n", " pass\n", "\n", + "\n", "def answer_node(state: InputState):\n", " return {\"answer\": \"bye\"}\n", "\n", diff --git a/docs/docs/how-tos/memory/delete-messages.ipynb b/docs/docs/how-tos/memory/delete-messages.ipynb index 805407307..840558807 100644 --- a/docs/docs/how-tos/memory/delete-messages.ipynb +++ b/docs/docs/how-tos/memory/delete-messages.ipynb @@ -163,7 +163,7 @@ " # Next, we pass in the function that will determine which node is called next.\n", " should_continue,\n", " # Next, we pass in the path map - all the possible nodes this edge could go to\n", - " ['action', END]\n", + " [\"action\", END],\n", ")\n", "\n", "# We now add a normal edge from `tools` to `agent`.\n", diff --git a/docs/docs/how-tos/memory/manage-conversation-history.ipynb b/docs/docs/how-tos/memory/manage-conversation-history.ipynb index 3b3370f30..bc0b84969 100644 --- a/docs/docs/how-tos/memory/manage-conversation-history.ipynb +++ b/docs/docs/how-tos/memory/manage-conversation-history.ipynb @@ -154,7 +154,7 @@ " # Next, we pass in the function that will determine which node is called next.\n", " should_continue,\n", " # Next, we pass in the path map - all the possible nodes this edge could go to\n", - " ['action',END]\n", + " [\"action\", END],\n", ")\n", "\n", "# We now add a normal edge from `tools` to `agent`.\n", @@ -291,7 +291,7 @@ " # Next, we pass in the function that will determine which node is called next.\n", " should_continue,\n", " # Next, we pass in the pathmap - all the possible nodes this edge could go to\n", - " ['action', END]\n", + " [\"action\", END],\n", ")\n", "\n", "# We now add a normal edge from `tools` to `agent`.\n", diff --git a/docs/docs/how-tos/pass-config-to-tools.ipynb b/docs/docs/how-tos/pass-config-to-tools.ipynb index cb65edfbe..a1e8df6ac 100644 --- a/docs/docs/how-tos/pass-config-to-tools.ipynb +++ b/docs/docs/how-tos/pass-config-to-tools.ipynb @@ -204,11 +204,7 @@ "workflow.add_node(\"tools\", tool_node)\n", "\n", "workflow.add_edge(START, \"agent\")\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " should_continue,\n", - " [\"tools\",END]\n", - ")\n", + "workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n", "workflow.add_edge(\"tools\", \"agent\")\n", "\n", "app = workflow.compile()" diff --git a/docs/docs/how-tos/pass-run-time-values-to-tools.ipynb b/docs/docs/how-tos/pass-run-time-values-to-tools.ipynb index 08ca92e5b..df32cdd8c 100644 --- a/docs/docs/how-tos/pass-run-time-values-to-tools.ipynb +++ b/docs/docs/how-tos/pass-run-time-values-to-tools.ipynb @@ -102,7 +102,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "1d36e782-80f4-4334-b7d7-ee4c79864480", "metadata": {}, "outputs": [], @@ -112,6 +112,7 @@ "\n", "from langchain_core.documents import Document\n", "from langchain_core.tools import tool\n", + "from langchain_core.messages import ToolMessage\n", "from langgraph.prebuilt import InjectedState\n", "\n", "from pydantic import BaseModel\n", @@ -553,7 +554,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/docs/docs/how-tos/persistence.ipynb b/docs/docs/how-tos/persistence.ipynb index 0784dd54c..36b28e56a 100644 --- a/docs/docs/how-tos/persistence.ipynb +++ b/docs/docs/how-tos/persistence.ipynb @@ -1,584 +1,585 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# How to add persistence (\"memory\") to your graph\n", - "\n", - "Many AI applications need memory to share context across multiple interactions. In LangGraph, memory is provided for any [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph) through [Checkpointers](https://github.com/langchain-ai/langgraph/tree/e4ca7ab69c599fd77dd4f0d47280849d715392cc/libs/checkpoint).\n", - "\n", - "When creating any LangGraph workflow, you can set them up to persist their state by doing using the following:\n", - "\n", - "1. A [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver).\n", - "2. Call `compile(checkpointer=my_checkpointer)` when compiling the graph.\n", - "\n", - "There are several options for checkpointers to use.\n", - "\n", - "1. [MemorySaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#memorysaver) is an in-memory key-value store for Graph state.\n", - "2. [SqliteSaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#sqlitesaver) allows you to save to a Sqlite db locally or in memory.\n", - "3. There are various external databases that can be used for persistence, such as [Postgres](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/), [MongoDB](https://langchain-ai.github.io/langgraph/how-tos/persistence_mongodb/), and [Redis](https://langchain-ai.github.io/langgraph/how-tos/persistence_redis/).\n", - " \n", - "Here is an example using [MemorySaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#memorysaver) in memory:\n", - "```python\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "builder = StateGraph(....)\n", - "# ... define the graph\n", - "memory = MemorySaver()\n", - "graph = builder.compile(checkpointer=memory)\n", - "...\n", - "```\n", - "\n", - "This works for [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph) and all its subclasses, such as [MessageGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#messagegraph).\n", - "\n", - "Below is an example.\n", - "\n", - "
\n", - "

Note

\n", - "

\n", - " In this how-to, we will create our agent from scratch to be transparent (but verbose). You can accomplish similar functionality using the create_react_agent(model, tools=tool, checkpointer=checkpointer) (API doc) constructor. This may be more appropriate if you are used to LangChain’s AgentExecutor class.\n", - "

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "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 OpenAI (the LLM we will use) and Tavily (the search tool we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "
\n", - "

Set up LangSmith for LangGraph development

\n", - "

\n", - " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here. \n", - "

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "4cf509bc", - "metadata": {}, - "source": [ - "## Define graph state\n", - "\n", - "The state is the interface for all the nodes." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "14619607", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "# Add messages essentially does this with more\n", - "# robust handling\n", - "# def add_messages(left: list, right: list):\n", - "# return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]" - ] - }, - { - "cell_type": "markdown", - "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", - "metadata": {}, - "source": [ - "## Define tools\n", - "\n", - "We will first define the tools we want to use.\n", - "For this simple example, we will use create a placeholder search engine.\n", - "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/how_to/custom_tools) on how to do that.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " return [\"The answer to your question lies within.\"]\n", - "\n", - "\n", - "tools = [search]" - ] - }, - { - "cell_type": "markdown", - "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", - "metadata": {}, - "source": [ - "Now we can create our [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/?h=tool+node#toolnode). This \n", - "object actually **runs** the tools (aka functions) that the LLM has asked to use." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tool_node = ToolNode(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", - "metadata": {}, - "source": [ - "## Define the model\n", - "\n", - "Now we need to load the [chat model](https://python.langchain.com/docs/concepts/#chat-models) to power our agent.\n", - "For the design below, it must satisfy two criteria:\n", - "\n", - "1. It should work with **messages** (since our state contains a list of chat messages)\n", - "2. It should work with [**tool calling**](https://python.langchain.com/docs/concepts/#functiontool-calling).\n", - "\n", - "
\n", - "

Note

\n", - "

\n", - " These model requirements are not general requirements for using LangGraph - they are just requirements for this one example.\n", - "

\n", - "
\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] - }, - { - "cell_type": "markdown", - "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", - "metadata": {}, - "source": [ - "\n", - "After we've done this, we should make sure the model knows that it has these tools available to call.\n", - "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", - "metadata": {}, - "outputs": [], - "source": [ - "bound_model = model.bind_tools(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", - "metadata": {}, - "source": [ - "## Define nodes and edges \n", - "\n", - "We now need to define a few different nodes in our graph.\n", - "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/concepts/#langchain-expression-language-lcel).\n", - "There are two main nodes we need for this:\n", - "\n", - "1. The agent: responsible for deciding what (if any) actions to take.\n", - "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", - "\n", - "We will also need to define some edges.\n", - "Some of these edges may be conditional.\n", - "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", - "The path that is taken is not known until that node is run (the LLM decides).\n", - "\n", - "1. Conditional Edge: after the agent is called, we should either:\n", - " a. If the agent said to take an action, then the function to invoke tools should be called\n", - " b. If the agent said that it was finished, then it should finish\n", - "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", - "\n", - "Let's define the nodes, as well as a function to decide how what conditional edge to take." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the function that determines whether to continue or not\n", - "from typing import Literal\n", - "from langgraph.graph import END\n", - "\n", - "def should_continue(state: State):\n", - " \"\"\"Return the next node to execute.\"\"\"\n", - " last_message = state[\"messages\"][-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return END\n", - " # Otherwise if there is, we continue\n", - " return \"action\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state: State):\n", - " response = model.invoke(state[\"messages\"])\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": response}" - ] - }, - { - "cell_type": "markdown", - "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", - "metadata": {}, - "source": [ - "## Compile the graph\n", - "\n", - "We can now put it all together and define the graph!" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "812b4e70-4956-4415-8880-db48b3dcbad2", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.graph import StateGraph, START\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(State)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.add_edge(START, \"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " [\"action\", END]\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")" - ] - }, - { - "cell_type": "markdown", - "id": "bc9c8536-f90b-44fa-958d-5df016c66d8f", - "metadata": {}, - "source": [ - "### Persistence\n", - "\n", - "To add in persistence, we pass in a checkpoint when compiling the graph" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6845ed6a-d155-4105-9160-28849877248b", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "memory = MemorySaver()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "79d29875-8aa8-434c-9f20-1c58346a6249", - "metadata": {}, - "outputs": [], - "source": [ - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile(checkpointer=memory)" - ] - }, - { - "cell_type": "markdown", - "id": "7654ebcc-2179-41b4-92d1-6666f6f8634f", - "metadata": {}, - "source": [ - "
\n", - "

Note

\n", - "

\n", - " If you're using LangGraph Cloud, you don't need to pass checkpointer when compiling the graph, since it's done automatically.\n", - "

\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "0d49697f", - "metadata": {}, - "outputs": [ - { - "data": { - "image/jpeg": "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", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph().draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", - "metadata": {}, - "source": [ - "## Use the graph\n", - "\n", - "We can now interact with the agent and see that it remembers previous messages!\n" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "cfd140f0-a5a6-4697-8115-322242f197b5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "hi! I'm bob\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Hello Bob! How can I assist you today?\n" - ] - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - "input_message = HumanMessage(content=\"hi! I'm bob\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "what is my name?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Your name is Bob.\n" - ] - } - ], - "source": [ - "input_message = HumanMessage(content=\"what is my name?\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "3f47bbfc-d9ef-4288-ba4a-ebbc0136fa9d", - "metadata": {}, - "source": [ - "If we want to start a new conversation, we can pass in a different thread id. Poof! All the memories are gone!" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "273d56a8-f40f-4a51-a27f-7c6bb2bda0ba", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "what is my name?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "I'm sorry, I do not know your name as I am an AI assistant and do not have access to personal information.\n" - ] - } - ], - "source": [ - "input_message = HumanMessage(content=\"what is my name?\")\n", - "for event in app.stream(\n", - " {\"messages\": [input_message]},\n", - " {\"configurable\": {\"thread_id\": \"3\"}},\n", - " stream_mode=\"values\",\n", - "):\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "e833f994", - "metadata": {}, - "source": [ - "All the checkpoints are persisted to the checkpointer, so you can always resume previous threads." - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "8578a66d-6489-4e03-8c23-fd0530278455", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "You forgot??\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "I apologize for the confusion. I am an AI assistant and I do not have the ability to remember information from previous interactions. How can I assist you today, Bob?\n" - ] - } - ], - "source": [ - "input_message = HumanMessage(content=\"You forgot??\")\n", - "for event in app.stream(\n", - " {\"messages\": [input_message]},\n", - " {\"configurable\": {\"thread_id\": \"2\"}},\n", - " stream_mode=\"values\",\n", - "):\n", - " event[\"messages\"][-1].pretty_print()" - ] - } - ], - "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.11.9" - } + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# How to add persistence (\"memory\") to your graph\n", + "\n", + "Many AI applications need memory to share context across multiple interactions. In LangGraph, memory is provided for any [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph) through [Checkpointers](https://github.com/langchain-ai/langgraph/tree/e4ca7ab69c599fd77dd4f0d47280849d715392cc/libs/checkpoint).\n", + "\n", + "When creating any LangGraph workflow, you can set them up to persist their state by doing using the following:\n", + "\n", + "1. A [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver).\n", + "2. Call `compile(checkpointer=my_checkpointer)` when compiling the graph.\n", + "\n", + "There are several options for checkpointers to use.\n", + "\n", + "1. [MemorySaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#memorysaver) is an in-memory key-value store for Graph state.\n", + "2. [SqliteSaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#sqlitesaver) allows you to save to a Sqlite db locally or in memory.\n", + "3. There are various external databases that can be used for persistence, such as [Postgres](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/), [MongoDB](https://langchain-ai.github.io/langgraph/how-tos/persistence_mongodb/), and [Redis](https://langchain-ai.github.io/langgraph/how-tos/persistence_redis/).\n", + " \n", + "Here is an example using [MemorySaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#memorysaver) in memory:\n", + "```python\n", + "from langgraph.graph import StateGraph\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "builder = StateGraph(....)\n", + "# ... define the graph\n", + "memory = MemorySaver()\n", + "graph = builder.compile(checkpointer=memory)\n", + "...\n", + "```\n", + "\n", + "This works for [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph) and all its subclasses, such as [MessageGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#messagegraph).\n", + "\n", + "Below is an example.\n", + "\n", + "
\n", + "

Note

\n", + "

\n", + " In this how-to, we will create our agent from scratch to be transparent (but verbose). You can accomplish similar functionality using the create_react_agent(model, tools=tool, checkpointer=checkpointer) (API doc) constructor. This may be more appropriate if you are used to LangChain’s AgentExecutor class.\n", + "

\n", + "
" + ] }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "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 OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"ANTHROPIC_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "
\n", + "

Set up LangSmith for LangGraph development

\n", + "

\n", + " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here. \n", + "

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "4cf509bc", + "metadata": {}, + "source": [ + "## Define graph state\n", + "\n", + "The state is the interface for all the nodes." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "14619607", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Annotated\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph.message import add_messages\n", + "\n", + "# Add messages essentially does this with more\n", + "# robust handling\n", + "# def add_messages(left: list, right: list):\n", + "# return left + right\n", + "\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Define tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use create a placeholder search engine.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/how_to/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " return [\"The answer to your question lies within.\"]\n", + "\n", + "\n", + "tools = [search]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "Now we can create our [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/?h=tool+node#toolnode). This \n", + "object actually **runs** the tools (aka functions) that the LLM has asked to use." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolNode\n", + "\n", + "tool_node = ToolNode(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Define the model\n", + "\n", + "Now we need to load the [chat model](https://python.langchain.com/docs/concepts/#chat-models) to power our agent.\n", + "For the design below, it must satisfy two criteria:\n", + "\n", + "1. It should work with **messages** (since our state contains a list of chat messages)\n", + "2. It should work with [**tool calling**](https://python.langchain.com/docs/concepts/#functiontool-calling).\n", + "\n", + "
\n", + "

Note

\n", + "

\n", + " These model requirements are not general requirements for using LangGraph - they are just requirements for this one example.\n", + "

\n", + "
\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "bound_model = model.bind_tools(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define nodes and edges \n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/concepts/#langchain-expression-language-lcel).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function that determines whether to continue or not\n", + "from typing import Literal\n", + "from langgraph.graph import END\n", + "\n", + "\n", + "def should_continue(state: State):\n", + " \"\"\"Return the next node to execute.\"\"\"\n", + " last_message = state[\"messages\"][-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return END\n", + " # Otherwise if there is, we continue\n", + " return \"action\"\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state: State):\n", + " response = model.invoke(state[\"messages\"])\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": response}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Compile the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "812b4e70-4956-4415-8880-db48b3dcbad2", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, START\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(State)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", tool_node)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " [\"action\", END],\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")" + ] + }, + { + "cell_type": "markdown", + "id": "bc9c8536-f90b-44fa-958d-5df016c66d8f", + "metadata": {}, + "source": [ + "### Persistence\n", + "\n", + "To add in persistence, we pass in a checkpoint when compiling the graph" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6845ed6a-d155-4105-9160-28849877248b", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "79d29875-8aa8-434c-9f20-1c58346a6249", + "metadata": {}, + "outputs": [], + "source": [ + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "markdown", + "id": "7654ebcc-2179-41b4-92d1-6666f6f8634f", + "metadata": {}, + "source": [ + "
\n", + "

Note

\n", + "

\n", + " If you're using LangGraph Cloud, you don't need to pass checkpointer when compiling the graph, since it's done automatically.\n", + "

\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0d49697f", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAD5AOADASIAAhEBAxEB/8QAHQABAAICAwEBAAAAAAAAAAAAAAYHBAUCAwgBCf/EAFAQAAEEAQICAwkLBwoEBwAAAAEAAgMEBQYRBxITITEUFRYXIkFVVpQIMjZRYXSTstHS0yNCVHGBkZUYNWJyc3Wxs7TBJSg0oTNDUldklvD/xAAbAQEBAAMBAQEAAAAAAAAAAAAAAQIDBAUGB//EADURAQABAgEJBQYGAwAAAAAAAAABAhEDBBIhMUFRUpHRFDNhcaEFExWBscEiI2KS8PFCwuH/2gAMAwEAAhEDEQA/AP1TREQEREBERARFGZ7V3VViatjrUuNxcLzHNkImt6Sd46nMhLgQADuHP2333DdiOYbKKM7baFs39u/WoNDrNiKu09hleGg/vWF4VYX0xQ9pZ9qw6nD/AE5TkMow9WxZJ3dats7ond/Wlk5nn9p86zPBXC+h6HszPsWy2DG2Z5R95NB4VYX0xQ9pZ9qeFWF9MUPaWfangrhfQ9D2Zn2J4K4X0PQ9mZ9ifk+Poug8KsL6Yoe0s+1PCrC+mKHtLPtTwVwvoeh7Mz7E8FcL6HoezM+xPyfH0NB4VYX0xQ9pZ9qeFWF9MUPaWfangrhfQ9D2Zn2J4K4X0PQ9mZ9ifk+PoaGbVu17zC+tYisMH50Tw4f9l3qP2tAafsvEjMVXp2RuW2qLe55mk9pD49nfF5/MuFS7d07dr0MpO69SsOEVTJOaA8P26op9thudvJeAA4+SQHcpkmZTV3c6d0/b+QltyRoiLQgiIgIiICIiAiIgIiICIiAiIg0OuMlPi9MW5KjxFcmdHUryH8yWaRsTHfsdID+xbTGY2vh8dWo1IxFWrRtijYOvZoGw/WtHxGYRpWW0AS2hZq33hreY8kNiOV/V/VY5SUEEAg7g+ddE9zT5z9IXY+oiLnRDOIPGLSHC6ehBqXLGlZvCR9evDVmsyvYzbnfyQse4MbzDdxAaN+sqLS+6MwsPHGLh66necJsXWvRZCHH25WPlnl5WRnlhLWxhvK4zOdyAuLSQWOCj3unKzqdzD5vBYzWMeuqNK2MPmtK403YmuPIe5bbNi0xSODD5bdhyE8zT240OQ1NpfjnpnVepNLZWx380VTxFyTA0n3IqWRbZdLLHJybmOP8AKnZ7vJ8k9aCxKfH7QV/XHghFnuXPGzJSZDNTniiknj354mTOjET3jld5LXE9R6lj3PdE6FrZXM4qHJ27+VxEk8F2pRxVyw6CSKIyua8xwuDd2g8p7HkEN5iCF5xzmP1nqLOacv6hw2v8nqvEa7r38hHHBMMJSx0d1zY3VY2kRzjoXRnmYHydchcQN1evA3TF7G2OL/dmOnx8mU1jdnry2YHR90QurV2skaSPKZuHAOG43DtvOg3fAXjTR45aAx+oa1K1jrUsEclqpPVnjjie8E8scskbGzAbe/j3H6t1ZCpX3J1+9Q4R4LR+X09m8DmdM0YqFzvnRfDBLI0ubvBKfJmb5G/MwkbOb8aupAWDnMRDnsRbx9jcR2Iyzmb1OYfM4HzEHYgjsICzl1zzx1oJJpXBkUbS97j2AAbkrKmZiYmnWNVo3LzZ3S2MvWeXuqSECfk970rfJk2+TmDtluVG+HVeSDRWLdKx0clhjrRY4bOb0rnSbEeYjn2Kki2Y0RGLVFOq8rOsREWlBERAREQEREBERAREQEREHGSNk0bo5Gh7HAtc1w3BB7QQotjLzdFCDEZOVsWNaRFj78rvILeoNhkcex47ASfLG23lbhStddivFbgkhnjZNDI0tfHI0Oa4HtBB7QttFcRE01aYlYlDdScEuH2scxPls7onAZjKTholuXsdFLK/laGt3c5pJ2AAHyALXO9zfwpe1gdw40u4MHK0HEwHlG5Ow8n4yT+1SAcPqNV3/Db2TxEe+/Q07rxCP6sb+ZrR8jQAvngTY9as99ND+Es8zDnVXzjpctG9m6S0Tp/QWNfjtN4ShgaEkpnfWx1dkEbpCAC8taANyGtG/wAgW7UX8CbHrVnvpofwk8CbHrVnvpofwk93h8fpJaN6UIqrgx+Vk4rXtPO1TmO90OFr32ESw9J0r55mO3PR+95Y27dXbv1qWeBNj1qz300P4Se7w+P0ktG9lav0FpriBTgqamwOOz9WCTpYoclVZOxj9iOYBwOx2JG/yqKfyauE3/ttpb+EQfdUh8CbHrVnvpofwk8CbHrVnvpofwk93h8fpJaN7o0nwi0PoHJSZHTekcLgLz4jA+1jqMcEjoyQ4tLmgHbdrTt8g+Jdl+xHrvmxtMtmwXNy37Y36Oy3zwRHseD2PcNwBuwbuJ5OwcPsfYP/ABKzkM0zc/kchbe+E79odENmOHyOaf8AuVJY42Qxtjja1kbAGta0bAAdgASKqMPTRN55W/nyXRGpyREXOxEREBERAREQEREBERAREQEREBERAREQEREFe1CP5QOUG55vBip1fJ3XZ+X/AGVhKvam/wDKAynZt4MVPMN/+rs/t/8A361YSAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgryoP+YPKnmG/gvT8nbr/wCrsqw1XlTb+UHlfj8F6fm/+XZ86sNAREQEREBERAREQEREBERAREQEREBERAREQEREBFDrGr8rkpZXYKhTmpRvdGLd6w+MTOa7lcWNaxxLNwRzEjfbcAtIcenv7rD9Awftc34a64yXE22j5wtk3RQjv7rD9Awftc34ad/dYfoGD9rm/DV7LXvjnBZ42xHu98vf90S/Ex8KZ26ltsh00cW/MtBjnjsSkuc/ufsBkO/V1BpK/QBeaaHACbHe6Hu8XYsfhu/Vmn0HcndEoiZORyPsAiP3zowGkfK49p6rf7+6w/QMH7XN+GnZa98c4LJuihHf3WH6Bg/a5vw07+6w/QMH7XN+GnZa98c4LJuiiFHV2Tp2oI87Qq1688jYWW6Nh8rWSOIDWyNcxpaCSAHAkbkb7bqXrRiYdWHNqi1hERakEREBERAREQEREBERAREQEREBERBXPDg82gsA49ppRE/rLQpGo3w3+AGnvmMX1QpIvZx+9r85+qzrkREWlBERARYIzmPObOHF2A5UVxbNISDpRCXcgkLe0NLgQD2Eg/EVnII9r47aXsEdolgI+Q9MxWIq71/8FbP9rB/nMViLXlHdUec/ZlsERFwMRERAREQEREBERAREQEREBERAREQVxw3+AGnvmMX1QpIo3w3+AGnvmMX1QpIvZx+9r85+qzrl5Cs6z1Cdd6Z1tpu5qQaUy2s2YR0+Z1AZa9yKSxJBI2HH9Hyxxtc13I/na/8AJgkHfdYzczqHVGua9Y6i1W7XsGvGwZDT1exYix0GHjsc4cWM2jEfQNjf0m/M9zuU8wcWq/Jvc4cOrGUlyD9ODul9wZFnLcsNZXsiQS9NCwScsLy8Al0YaT177gnetcn7nXV9jiHaymJsYjTFSbNd8++2LzWVFkxmfpZGOpOk7mLpBzNcfennceXzLjmmURPCni5xej1FqjT191HJ183cpUel1VLWq0RXnMbYZsc2o+OTyWgu53lzufcFu4A2Wse/+Rqe6Az41hqLHXdIym1iK1HJyR1a0keMgnIMY6pGOcNix+7etxDQXOJu7IcA9BZPVz9TT4BozEliO3LJDaniimnYQWSyQseI3vBAPM5pO47VuLPDHTVunq2rLjeeDVYcMyzp5R3VzQNgPWHbs/Jta3yOXs37etXNkUthdOV9V+6rqZuzfy1e1NonHZboamVsQwmTup4MZja8NdF1AmMgsJc4kbuJPpFQ3NcHtI5/KYHJXMU45DBxtgoWYLU0MkcbS0iNzmPaZGbtaeV/MNx2dZUyWcRYR7X/AMFbP9rB/nMViKu9f/BWz/awf5zFYixyjuqPOf8AVlsERFwMRERAREQEREBERAREQEREBERAREQVxw3+AGnvmMX1QpIoriZMphpLWMxuEt5zEVOTuS/XkjjEjHgPaxvSvaJOVrgOkaS09Q98HAZ/fbPepmV9qpfjr2q7Ylc101RaZvrjqymLzdu0Wk77Z71MyvtVL8dO+2e9TMr7VS/HWGZ+qP3R1LN2ir2DjJWs8Q7OhYsJefqutTbkJcaLFTnbCSAHc3Tcu/WDy777EHbbrUn77Z71MyvtVL8dMz9UfujqWbtFpO+2e9TMr7VS/HTvtnvUzK+1Uvx0zP1R+6OpZ06/+Ctn+1g/zmKxFXeQx+o9SUZIhg2Y+OLlsCHI3WNNqRhD44eaHpBGxzmgOf5RDd9mO36pjis/XyluzS5ZK+SqRwvtVJWEOi6RvM3Z23K8dTm8zCW8zHDfdpA5somM2mi95i+rTrt0J1WbNERcLEREQEREBERAREQEREBERARFhZPL1sS2ETyN6ew90VWtztbJZkEb5OjjDiOZ/JG923xNcTsASA7MhkquJrGxdsxVYOdkfSTPDQXvcGMaN+1znOa0DtJcAOsrSvx1jV0RGUrPpYlwtVZsRYEcovRO/JtdL28rS3ncIwdyHt5tiCwZGMxdi5MzJZbcWZYYHDGlzZYKMrWkuMbuUFzi57gXnbcNbsG9e+7QcYomQxsjjY2ONgDWsaNg0DsAC5IiAiIg/O/T/uZuOGP917LraTUOlJtRRuZm7MYvWhDJTllkhNdpNckeRG5u22wG2x+L9EFX9ACbj7nHM5SYNM0GybE7jntXC0Edn/luVgICIiAsLKYepmYoGW4ukEE8dmIhxa5kjHBzXAgg9o2I7CCWncEg5qII7Hkr+nOjizDzfqOdZkdl442Qx1YmjnYJxzdvLzDnaOUlm5DeYBb6vYitwRzwSMmhlaHskjcHNe0jcEEdoI867FHrOLuafElrBxd1RNhggZhHTNgrxsY/ZzoTyHkf0ZIDDsxxYwbx7uegkKLDxmXp5hlh1Ow2cV55K0wbuDHKw7OaQesH/EEEbggrMQEREBERAREQEREBERBjZG63G4+1bfFNO2vE6UxVozJK8NBOzGDrc47bADrJ6lgYWlPK92SvmU2p/wApDWsxQh+PY5jOaAOj3362AuPO/d2+x5Q0DE1VSdlMlp6nJi5r9Hu4WZ547PRMrOhY6SJ72jrkHSNYA3s32J7NlI0BERAREQF03LkGPqT2rU8darAx0ss0zwxkbGjdznOPUAACSSu5V7ZceLGUbWhdzaKoT72ZWkgZaxG7qhafPXY4bvI6pHN5PeNkDw7+FlWxko8zq67DJXsajsNs168oLXwUWMDKzHNPvXOaDK5p62unc09inaIgIiICIiAiIg1WWxkzpo8hQfIL9ZkpZVNgw17bnM2DJvJfsOZrCJA0vby9Xkue12Ti8pBla7pIXs6SN3RzwtkY90EoALo38pIDm7jcbrMUYyluDTmrsVPLer06uae+gavce77N0R9JE8zNHk7RQTNIf1H8mAQQGvCToiICIiAiIgIi0uY1tp7T9oVsnnMdj7JHN0Nm0xj9vj5Sd9lnTRVXNqYvK2u3SKLeNLR3rTiPbY/tTxpaO9acR7bH9q29nxuCeUrmzufdeGtQr4rOWaTbTcNebadO+6KrKUTmPhmsPc4hrmRxSyOc13VsNx5TWrYaX1jgNb499/TucxufoslMLrWLtx2YmyAAlhcwkBwBB27esfGvHvu9eEekeOmlodVac1Dipda4OuY2QMvRk3qoJeYQOb37S5zm7dvM4de42sv3HsuluE3uedJ4S/ncVRy0kLrt6GS0xj2zSuLy1wJ3DmtLWkHr8lOz43BPKTNnc9Hoot40tHetOI9tj+1PGlo71pxHtsf2p2fG4J5SZs7kpRRbxpaO9acR7bH9qg9jiBg+KORsULGfx+L0fDIYZYZLjI7OZcDylrgTvFV36vM+Y9Xkxf8Ajuz43BPKUzZ3JFNem4qzuq42d8GjY3Pjt5CJxY/KHYgxV3ggiLc+VK0+Vy8rOolwnNSpBj6kNWrDHWrQMbHFDCwMZGwDZrWtHUAAAAAuVeGKvBHFAxkUMbQ1jIwA1rQNgAB2DZdi50EREBERAREQEREBRvXOUGJx+MlOZdhBJlaVcytrdP0/SWGMEG35vSFwZz/m82/mUkVbcSuM2idIW4cVk+JOntJ5eG7UdPWuWoHz9EZWOMbonO5mNkYdukI2YHc++w3QWSi12ntSYnV2Hr5bBZSlmsVY5uhvY+wyeCXlcWu5XsJadnNcDseogjzLYoCIiAiIgws1cdj8PetMAL4IJJWg/G1pI/wUR0lUjrYClIBzT2YmTzzO63zSOaC57ieskk/s7OwKT6q+DGY+ZzfUKj2mvg5ivmkX1AvQwNGFPmuxskRFmgiIgIiIC+PY2Rpa5oc09RBG4K+ogxOHj+gbnsXH1VMbkOgrR7dUUboIZeRv9EGVwA7ANmgAAKXKHaB/njWX96x/6KqpiubKe9nyj6Qs6xERcqCIiAuMsrIY3ySPbHGwFznuOwaB2klYmZy9TAYuzkb0ohq12F8jz8XxAeck7ADzkgLzzq3U93XVsy5HmjotcHQYzm5oo9uwvA6nv8+53A/N26yfUyHIK8tqm02pjXPRfNctni3o+q8tdn6kpHaa5Mw/ewELo8c2jfTTfZ5fuKjgA0AAbAdQARfRx7DyfbVV6dEvC8fHNo30032eX7i8a+764ZYXjjb0tqPR1yKxqCGZuMvNMT2A1nu3bM4lo6o3F2/adnf0VbKJ8Dybiq5x0Lwsvhpqjh1ws0DgtJ4jLsbj8TVbXY7uaUGQjrfIRy++c4ucflcVJvHNo30032eX7io5E+B5NxVc46F4Xj45tG+mm/QS/cW6wetsBqWQxYzL07k4HMYY5R0gHx8h8rb5dvMvOi65q0c5aXt8phDmPB2cwg7gtcOsHcdoWFfsPAmPwVzE+Np+0F4eqkVVcL+I9mxciwOamNiaQHuK8/bmk2G5ik+N4AJDvzgCD5Q3faq+VynJsTJcScPE/savVXwYzHzOb6hUe018HMV80i+oFIdVfBjMfM5vqFR7TXwcxXzSL6gW/B7mfP7LsbJUnp33Rstvixj9DZ7A4/EXck+eKqaWoK+QnjkijdJy2YGAOh5mMcQd3Dcbb7q5rkBtVJ4WyvgdIxzBLGdnMJG24+ULzhob3PettL2+GsUvgjFQ0XefI6al04s5RkkMkMk8jizZku0nOWeWHOJ8toHXJvosie6J4yah4jXYshp7Q/dGiJrUlaHP2ctHDLM1j3MdOytyEmLmaQCXhxHXyqHcLuJ3EbUHDLXmVzeDpWpMfcysVaWDN9FITDYewwAiqAwRsaQ2Xyi7kBLQSdpDwt4e8ROFEGP0lQs6av6FoWnmtcsmw3JMqOkc/oTG1vRue3m5RJzgbAEt3XZozhjrDSlbXOnXTYSzpbL2Mnex9oSzNuxy23mQRys5Czka57xzNcSRy+SOtTTtGp0lxryk2mtB4HSemrertQXNK089bGXzTYzWryMaGGa0YiZZnu5h1MHNylx5Qsuj7pG3qifSNPS+kJMlktQUr87q9/INqChNTmjhmimdyP6g5zxzMDju1vk7OLm4Gn+DGueHLdIZXStnT1vOU9JUtMZenlZZ2VZXVm7xzwyMjLtw50g5XNHM0j3pCzeG3ADKaC1TonJy5Srke9dDLjKz7OjksXL1iKdzo2bEBgLHjrcDty9R3O0jOFhcK+IbOJmkxljj5MTdht2cfdx8kglNazBK6KVnOAA4czSQ4AbgjqHYpeoJwg0HkOH+J1DVyM1aaTIahyWWiNVznBsViw+VjXczR5Qa4AgbjfsJ7VO1sjVpGDoH+eNZf3rH/oqqmKh2gf541l/esf8AoqqmK58q7z5R9IWRERcqCIiCqOPOTeIcBiQSI7M8lqQDse2Fo2afk55GO/WwKsFaHHrGPMGBywBMdWeSrKR2MbMBs4/Jzxsb+t4VXr9A9k5vY6c3xv536WKtgih83FfAQTPifFm+Zji08un8g4bj4iINj+sL4/i1p9ji0xZzcHbq09kCP39AvS99h8Uc2CK6090JR0xqPKYmpBjLbsUGi4b+bgoyF5aH8kMb9zKQ0jfflG5233B2ymcZr2dyPc2l9M9+mOw1XNsmsX21QYpuk2YRyOIf5A2HWDudy3Yb48WkNTY/P5nN6QdhrOK1G6O8+DUEM8MtSfo2sLmtDOZwcGtJY/kII23ClGN0ddqcSMvqB8lbuO5iatBkcZcHtkjfK5xI22DdpBt1k9R6lx0+/qq01aL7o1abWnkrSjjO7N1tNR6Wwcmcymbx/fQVJrLazKtbqBdLIWu2POeUAA7kHsAXdwJzGSzuj8hayr5zc79ZCN0VifpnQBth4EQdudwweSNurYdXUo1pnhJqzQtXSN7C2sPYzONw/eXI1rr5RWni6TpGvjkazmDmu37W7EO8y3ei5zwlwcuO1K6WxkbuQuZEuwuNuW4Q2WZzwN2RO5T5XYf3ntUw6sXPirG0aPC2z/os9FDfG3p/bfos5/8AXch+Atxp3WGO1S6dtBl9pgDS/u3G2anbvty9NG3m7D2b7eftC74xaKptFUT80bLITS1Kj7UDujs1drML9t+WRh5mn94C9TULjchRrWmDZk8bZWj5HAH/AHXlnIQy26rqtdvSWbRFaFm+3M955Wj95XqehUZj6NerH1sgjbE39QGw/wAF8z7ezbYe/T9mcamFqr4MZj5nN9QqPaa+DmK+aRfUClOZpuyOIvVGEB88EkQJ8xc0j/dRDSVyOxgacIPJZrQsgsQO6nwyNaA5jgesEH942I6iF4OBpwpjxXY3CIizQREQEREBEXGSRsTC97gxg6y5x2AQYegf541l/esf+iqqYqI8PGd0MzmUj66mTv8AT1pN+qWNsEUQkb/RcYnFp7HDZwJDgpcubKe9nyj0iFnWIiLlQREQYeYxNXPYyzj7sQmq2GGORh84PnB8xHaD5iAV551dpe9oW0Y8hzS48kCHKcu0b9+wP26mP82x2DvzfOB6SXGSNssbmPaHscC1zXDcEHtBC9PIsvryKqbRemdcL5vLTXB7Q5pBB6wR519V82uE2j7b3Pdp6lE53We54+hB8/YzZdHib0b6Di+lk+8vo49uZPtpq9OqWhRqK8vE3o30HF9LJ95PE3o30HF9LJ95X45k3DVyjqWhRqK8vE3o30HF9LJ95PE3o30HF9LJ95PjmTcNXKOpaFGrrlsxwuY1zt5HnZkbQXPeewBrR1k/IAr28TejfQcX0sn3lusHozA6aeX4vEU6MpGxlhhaJCPiLu0j9qwr9uYMR+CiZnxtHUtCC8L+G9mpcjz2bh6CdgPcVF3W6LcEGWT4nkEgNHvQTvu52zLURF8rlOU4mVYk4mJ/QLS5jRWn9Q2BYymDxuRnA5RLaqRyPA+LdwJ2W6Rc9NdVE3pm0mpFvFXoz1Twn8Pi+6nir0Z6p4T+HxfdUpRbu0Y3HPOVvO9FvFXoz1Twn8Pi+6nir0Z6p4T+HxfdUpRO0Y3HPOS870W8VejPVPCfw+L7qeKvRnqnhP4fF91SlE7Rjcc85LzvRbxV6M9U8J/D4vursg4Z6QrSCSLS+GjeOsObQiB7d/8A0/GApKidoxp/znnJeRERc6CIiAiIgIiICIiAiIgIiICIiAiIg//Z", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(app.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "## Use the graph\n", + "\n", + "We can now interact with the agent and see that it remembers previous messages!\n" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "hi! I'm bob\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Hello Bob! How can I assist you today?\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "input_message = HumanMessage(content=\"hi! I'm bob\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "what is my name?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Your name is Bob.\n" + ] + } + ], + "source": [ + "input_message = HumanMessage(content=\"what is my name?\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "3f47bbfc-d9ef-4288-ba4a-ebbc0136fa9d", + "metadata": {}, + "source": [ + "If we want to start a new conversation, we can pass in a different thread id. Poof! All the memories are gone!" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "273d56a8-f40f-4a51-a27f-7c6bb2bda0ba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "what is my name?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "I'm sorry, I do not know your name as I am an AI assistant and do not have access to personal information.\n" + ] + } + ], + "source": [ + "input_message = HumanMessage(content=\"what is my name?\")\n", + "for event in app.stream(\n", + " {\"messages\": [input_message]},\n", + " {\"configurable\": {\"thread_id\": \"3\"}},\n", + " stream_mode=\"values\",\n", + "):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "e833f994", + "metadata": {}, + "source": [ + "All the checkpoints are persisted to the checkpointer, so you can always resume previous threads." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "8578a66d-6489-4e03-8c23-fd0530278455", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "You forgot??\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "I apologize for the confusion. I am an AI assistant and I do not have the ability to remember information from previous interactions. How can I assist you today, Bob?\n" + ] + } + ], + "source": [ + "input_message = HumanMessage(content=\"You forgot??\")\n", + "for event in app.stream(\n", + " {\"messages\": [input_message]},\n", + " {\"configurable\": {\"thread_id\": \"2\"}},\n", + " stream_mode=\"values\",\n", + "):\n", + " event[\"messages\"][-1].pretty_print()" + ] + } + ], + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/docs/docs/how-tos/persistence_redis.ipynb b/docs/docs/how-tos/persistence_redis.ipynb index a3c5ec33c..66106253b 100644 --- a/docs/docs/how-tos/persistence_redis.ipynb +++ b/docs/docs/how-tos/persistence_redis.ipynb @@ -256,9 +256,7 @@ " }\n", " }\n", "\n", - " checkpoint = serde.loads_typed(\n", - " (data[b\"type\"].decode(), data[b\"checkpoint\"])\n", - " )\n", + " checkpoint = serde.loads_typed((data[b\"type\"].decode(), data[b\"checkpoint\"]))\n", " metadata = serde.loads(data[b\"metadata\"].decode())\n", " parent_checkpoint_id = data.get(b\"parent_checkpoint_id\", b\"\").decode()\n", " parent_config = (\n", diff --git a/docs/docs/how-tos/react-agent-from-scratch.ipynb b/docs/docs/how-tos/react-agent-from-scratch.ipynb index e815facd4..f6a1860a1 100644 --- a/docs/docs/how-tos/react-agent-from-scratch.ipynb +++ b/docs/docs/how-tos/react-agent-from-scratch.ipynb @@ -82,6 +82,7 @@ "from langchain_core.messages import BaseMessage\n", "from langgraph.graph.message import add_messages\n", "\n", + "\n", "class AgentState(TypedDict):\n", " \"\"\"The state of the agent.\"\"\"\n", "\n", @@ -108,16 +109,18 @@ "\n", "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", "\n", + "\n", "@tool\n", "def get_weather(location: str):\n", " \"\"\"Call to get the weather from a specific location.\"\"\"\n", " # This is a placeholder for the actual implementation\n", " # Don't let the LLM know this though 😊\n", - " if any([city in location.lower() for city in ['sf','san francisco']]):\n", + " if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n", " return \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", " else:\n", " return f\"I am not sure what the weather is in {location}\"\n", "\n", + "\n", "tools = [get_weather]\n", "\n", "model = model.bind_tools(tools)" @@ -145,13 +148,13 @@ "from langchain_core.runnables import RunnableConfig\n", "\n", "tools_by_name = {tool.name: tool for tool in tools}\n", + "\n", + "\n", "# Define our tool node\n", "def tool_node(state: AgentState):\n", " outputs = []\n", - " for tool_call in state['messages'][-1].tool_calls:\n", - " tool_result = tools_by_name[tool_call[\"name\"]].invoke(\n", - " tool_call[\"args\"]\n", - " )\n", + " for tool_call in state[\"messages\"][-1].tool_calls:\n", + " tool_result = tools_by_name[tool_call[\"name\"]].invoke(tool_call[\"args\"])\n", " outputs.append(\n", " ToolMessage(\n", " content=json.dumps(tool_result),\n", @@ -161,17 +164,21 @@ " )\n", " return {\"messages\": outputs}\n", "\n", + "\n", "# Define the node that calls the model\n", "def call_model(\n", " state: AgentState,\n", " config: RunnableConfig,\n", "):\n", " # this is similar to customizing the create_react_agent with state_modifier, but is a lot more flexible\n", - " system_prompt = SystemMessage(\"You are a helpful AI assistant, please respond to the users query to the best of your ability!\")\n", - " response = model.invoke([system_prompt] + state['messages'], config)\n", + " system_prompt = SystemMessage(\n", + " \"You are a helpful AI assistant, please respond to the users query to the best of your ability!\"\n", + " )\n", + " response = model.invoke([system_prompt] + state[\"messages\"], config)\n", " # We return a list, because this will get added to the existing list\n", " return {\"messages\": [response]}\n", "\n", + "\n", "# Define the conditional edge that determines whether to continue or not\n", "def should_continue(state: AgentState):\n", " messages = state[\"messages\"]\n", @@ -308,6 +315,7 @@ " else:\n", " message.pretty_print()\n", "\n", + "\n", "inputs = {\"messages\": [(\"user\", \"what is the weather in sf\")]}\n", "print_stream(graph.stream(inputs, stream_mode=\"values\"))" ] diff --git a/docs/docs/how-tos/react-agent-structured-output.ipynb b/docs/docs/how-tos/react-agent-structured-output.ipynb index 79b09d751..9d2d86ade 100644 --- a/docs/docs/how-tos/react-agent-structured-output.ipynb +++ b/docs/docs/how-tos/react-agent-structured-output.ipynb @@ -112,22 +112,28 @@ "outputs": [], "source": [ "from pydantic import BaseModel, Field\n", - "from typing import Literal\n", + "from typing import Literal\n", "from langchain_core.tools import tool\n", "from langchain_anthropic import ChatAnthropic\n", "from langgraph.graph import MessagesState\n", "\n", + "\n", "class WeatherResponse(BaseModel):\n", " \"\"\"Respond to the user with this\"\"\"\n", + "\n", " temperature: float = Field(description=\"The temperature in fahrenheit\")\n", - " wind_directon: str = Field(description=\"The direction of the wind in abbreviated form\")\n", + " wind_directon: str = Field(\n", + " description=\"The direction of the wind in abbreviated form\"\n", + " )\n", " wind_speed: float = Field(description=\"The speed of the wind in km/h\")\n", "\n", - "# Inherit 'messages' key from MessagesState, which is a list of chat messages \n", + "\n", + "# Inherit 'messages' key from MessagesState, which is a list of chat messages\n", "class AgentState(MessagesState):\n", " # Final structured response from the agent\n", " final_response: WeatherResponse\n", "\n", + "\n", "@tool\n", "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", " \"\"\"Use this to get weather information.\"\"\"\n", @@ -137,11 +143,12 @@ " return \"It is 75 degrees and sunny in SF, with 3 mph winds in the South-East direction\"\n", " else:\n", " raise AssertionError(\"Unknown city\")\n", - " \n", + "\n", + "\n", "tools = [get_weather]\n", - " \n", + "\n", "model = ChatAnthropic(model=\"claude-3-opus-20240229\")\n", - " \n", + "\n", "model_with_tools = model.bind_tools(tools)\n", "model_with_structured_output = model.with_structured_output(WeatherResponse)" ] @@ -170,33 +177,40 @@ "\n", "tools = [get_weather, WeatherResponse]\n", "\n", - "# Force the model to use tools by passing tool_choice=\"any\" \n", - "model_with_response_tool = model.bind_tools(tools,tool_choice=\"any\")\n", + "# Force the model to use tools by passing tool_choice=\"any\"\n", + "model_with_response_tool = model.bind_tools(tools, tool_choice=\"any\")\n", + "\n", "\n", "# Define the function that calls the model\n", "def call_model(state: AgentState):\n", - " response = model_with_response_tool.invoke(state['messages'])\n", + " response = model_with_response_tool.invoke(state[\"messages\"])\n", " # We return a list, because this will get added to the existing list\n", " return {\"messages\": [response]}\n", "\n", + "\n", "# Define the function that responds to the user\n", "def respond(state: AgentState):\n", " # Construct the final answer from the arguments of the last tool call\n", - " response = WeatherResponse(**state['messages'][-1].tool_calls[0]['args'])\n", + " response = WeatherResponse(**state[\"messages\"][-1].tool_calls[0][\"args\"])\n", " # We return the final answer\n", " return {\"final_response\": response}\n", "\n", + "\n", "# Define the function that determines whether to continue or not\n", "def should_continue(state: AgentState):\n", " messages = state[\"messages\"]\n", " last_message = messages[-1]\n", " # If there is only one tool call and it is the response tool call we respond to the user\n", - " if len(last_message.tool_calls) == 1 and last_message.tool_calls[0]['name'] == \"WeatherResponse\":\n", + " if (\n", + " len(last_message.tool_calls) == 1\n", + " and last_message.tool_calls[0][\"name\"] == \"WeatherResponse\"\n", + " ):\n", " return \"respond\"\n", " # Otherwise we will use the tool node again\n", " else:\n", " return \"continue\"\n", "\n", + "\n", "# Define a new graph\n", "workflow = StateGraph(AgentState)\n", "\n", @@ -239,7 +253,9 @@ "metadata": {}, "outputs": [], "source": [ - "answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']" + "answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})[\n", + " \"final_response\"\n", + "]" ] }, { @@ -292,21 +308,26 @@ "from langgraph.prebuilt import ToolNode\n", "from langchain_core.messages import HumanMessage\n", "\n", + "\n", "# Define the function that calls the model\n", "def call_model(state: AgentState):\n", - " response = model_with_tools.invoke(state['messages'])\n", + " response = model_with_tools.invoke(state[\"messages\"])\n", " # We return a list, because this will get added to the existing list\n", " return {\"messages\": [response]}\n", "\n", + "\n", "# Define the function that responds to the user\n", "def respond(state: AgentState):\n", " # We call the model with structured output in order to return the same format to the user every time\n", " # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n", " # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n", - " response = model_with_structured_output.invoke([HumanMessage(content=state['messages'][-2].content)])\n", + " response = model_with_structured_output.invoke(\n", + " [HumanMessage(content=state[\"messages\"][-2].content)]\n", + " )\n", " # We return the final answer\n", " return {\"final_response\": response}\n", "\n", + "\n", "# Define the function that determines whether to continue or not\n", "def should_continue(state: AgentState):\n", " messages = state[\"messages\"]\n", @@ -318,6 +339,7 @@ " else:\n", " return \"continue\"\n", "\n", + "\n", "# Define a new graph\n", "workflow = StateGraph(AgentState)\n", "\n", @@ -361,7 +383,9 @@ "metadata": {}, "outputs": [], "source": [ - "answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']" + "answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})[\n", + " \"final_response\"\n", + "]" ] }, { diff --git a/docs/docs/how-tos/recursion-limit.ipynb b/docs/docs/how-tos/recursion-limit.ipynb index 7c24e79fe..c6bf276de 100644 --- a/docs/docs/how-tos/recursion-limit.ipynb +++ b/docs/docs/how-tos/recursion-limit.ipynb @@ -139,7 +139,7 @@ "from langgraph.errors import GraphRecursionError\n", "\n", "try:\n", - " graph.invoke({\"aggregate\": []},{\"recursion_limit\":3})\n", + " graph.invoke({\"aggregate\": []}, {\"recursion_limit\": 3})\n", "except GraphRecursionError:\n", " print(\"Recursion Error\")" ] @@ -169,7 +169,7 @@ ], "source": [ "try:\n", - " graph.invoke({\"aggregate\": []},{\"recursion_limit\":4})\n", + " graph.invoke({\"aggregate\": []}, {\"recursion_limit\": 4})\n", "except GraphRecursionError:\n", " print(\"Recursion Error\")" ] diff --git a/docs/docs/how-tos/return-when-recursion-limit-hits.ipynb b/docs/docs/how-tos/return-when-recursion-limit-hits.ipynb index 7e9f3291a..ee9e32136 100644 --- a/docs/docs/how-tos/return-when-recursion-limit-hits.ipynb +++ b/docs/docs/how-tos/return-when-recursion-limit-hits.ipynb @@ -59,29 +59,34 @@ "from langgraph.graph import StateGraph\n", "from langgraph.graph import START, END\n", "\n", + "\n", "class State(TypedDict):\n", " value: str\n", " action_result: str\n", "\n", + "\n", "def router(state: State):\n", - " if state['value'] == \"end\":\n", + " if state[\"value\"] == \"end\":\n", " return END\n", " else:\n", " return \"action\"\n", "\n", + "\n", "def decision_node(state):\n", - " return {'value':'keep going!'}\n", + " return {\"value\": \"keep going!\"}\n", + "\n", "\n", "def action_node(state: State):\n", " # Do your action here ...\n", - " return {'action_result':'what a great result!'}\n", + " return {\"action_result\": \"what a great result!\"}\n", + "\n", "\n", "workflow = StateGraph(State)\n", - "workflow.add_node('decision',decision_node)\n", - "workflow.add_node('action',action_node)\n", - "workflow.add_edge(START,'decision')\n", - "workflow.add_conditional_edges('decision',router,['action',END])\n", - "workflow.add_edge('action','decision')\n", + "workflow.add_node(\"decision\", decision_node)\n", + "workflow.add_node(\"action\", action_node)\n", + "workflow.add_edge(START, \"decision\")\n", + "workflow.add_conditional_edges(\"decision\", router, [\"action\", END])\n", + "workflow.add_edge(\"action\", \"decision\")\n", "app = workflow.compile()" ] }, @@ -131,7 +136,7 @@ "from langgraph.errors import GraphRecursionError\n", "\n", "try:\n", - " app.invoke({\"value\":\"hi!\"})\n", + " app.invoke({\"value\": \"hi!\"})\n", "except GraphRecursionError:\n", " print(\"Recursion Error\")" ] @@ -168,34 +173,39 @@ " def __call__(self, step: int) -> bool:\n", " limit = self.config.get(\"recursion_limit\", 0)\n", " return step >= limit - 2\n", - " \n", + "\n", + "\n", "class State(TypedDict):\n", " value: str\n", " action_result: str\n", " is_last_step: Annotated[bool, IsLastOrSecondToLastStepManager]\n", "\n", + "\n", "def router(state: State):\n", " # Force the agent to end if it is on the last step\n", - " if state['is_last_step']:\n", + " if state[\"is_last_step\"]:\n", " return END\n", - " if state['value'] == \"end\":\n", + " if state[\"value\"] == \"end\":\n", " return END\n", " else:\n", " return \"action\"\n", "\n", + "\n", "def decision_node(state):\n", - " return {'value':'keep going!'}\n", + " return {\"value\": \"keep going!\"}\n", + "\n", "\n", "def action_node(state: State):\n", " # Do your action here ...\n", - " return {'action_result':'what a great result!'}\n", + " return {\"action_result\": \"what a great result!\"}\n", + "\n", "\n", "workflow = StateGraph(State)\n", - "workflow.add_node('decision',decision_node)\n", - "workflow.add_node('action',action_node)\n", - "workflow.add_edge(START,'decision')\n", - "workflow.add_conditional_edges('decision',router,['action',END])\n", - "workflow.add_edge('action','decision')\n", + "workflow.add_node(\"decision\", decision_node)\n", + "workflow.add_node(\"action\", action_node)\n", + "workflow.add_edge(START, \"decision\")\n", + "workflow.add_conditional_edges(\"decision\", router, [\"action\", END])\n", + "workflow.add_edge(\"action\", \"decision\")\n", "app = workflow.compile()" ] }, @@ -216,7 +226,7 @@ } ], "source": [ - "app.invoke({\"value\":\"hi!\"})" + "app.invoke({\"value\": \"hi!\"})" ] }, { diff --git a/docs/docs/how-tos/run-id-langsmith.ipynb b/docs/docs/how-tos/run-id-langsmith.ipynb index 21c67be40..d8a1d1bdc 100644 --- a/docs/docs/how-tos/run-id-langsmith.ipynb +++ b/docs/docs/how-tos/run-id-langsmith.ipynb @@ -136,8 +136,10 @@ " print(message)\n", " else:\n", " message.pretty_print()\n", + "\n", + "\n", "inputs = {\"messages\": [(\"user\", \"what is the weather in sf\")]}\n", - "config = {\"configurable\": {\"run_id\":\"12345\"}}\n", + "config = {\"configurable\": {\"run_id\": \"12345\"}}\n", "\n", "print_stream(graph.stream(inputs, config, stream_mode=\"values\"))" ] diff --git a/docs/docs/how-tos/state-model.ipynb b/docs/docs/how-tos/state-model.ipynb index addaaae08..1a0f6e69a 100644 --- a/docs/docs/how-tos/state-model.ipynb +++ b/docs/docs/how-tos/state-model.ipynb @@ -231,6 +231,7 @@ "\n", "from pydantic import BaseModel\n", "\n", + "\n", "class AgentState(BaseModel):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]" ] diff --git a/docs/docs/how-tos/streaming-content.ipynb b/docs/docs/how-tos/streaming-content.ipynb index 2f7b70a4a..f4fa01623 100644 --- a/docs/docs/how-tos/streaming-content.ipynb +++ b/docs/docs/how-tos/streaming-content.ipynb @@ -71,9 +71,10 @@ "from langgraph.graph import START, StateGraph, MessagesState, END\n", "from langgraph.types import StreamWriter\n", "\n", + "\n", "async def my_node(\n", - " state: MessagesState, \n", - " writer: StreamWriter # <-- provide StreamWriter to write chunks to be streamed\n", + " state: MessagesState,\n", + " writer: StreamWriter, # <-- provide StreamWriter to write chunks to be streamed\n", "):\n", " chunks = [\n", " \"Four\",\n", @@ -87,11 +88,12 @@ " \"...\",\n", " ]\n", " for chunk in chunks:\n", - " # write the chunk to be streamed using stream_mode=custom \n", + " # write the chunk to be streamed using stream_mode=custom\n", " writer(chunk)\n", "\n", " return {\"messages\": [AIMessage(content=\" \".join(chunks))]}\n", "\n", + "\n", "# Define a new graph\n", "workflow = StateGraph(MessagesState)\n", "\n", @@ -184,6 +186,7 @@ "from langchain_core.runnables import RunnableConfig, RunnableLambda\n", "from langchain_core.callbacks.manager import adispatch_custom_event\n", "\n", + "\n", "async def my_node(state: MessagesState, config: RunnableConfig):\n", " chunks = [\n", " \"Four\",\n", @@ -200,11 +203,12 @@ " await adispatch_custom_event(\n", " \"my_custom_event\",\n", " {\"chunk\": chunk},\n", - " config=config # <-- propagate config\n", + " config=config, # <-- propagate config\n", " )\n", "\n", " return {\"messages\": [AIMessage(content=\" \".join(chunks))]}\n", "\n", + "\n", "# Define a new graph\n", "workflow = StateGraph(MessagesState)\n", "\n", diff --git a/docs/docs/how-tos/streaming-events-from-within-tools.ipynb b/docs/docs/how-tos/streaming-events-from-within-tools.ipynb index 27cdc27a9..8de3b5f7a 100644 --- a/docs/docs/how-tos/streaming-events-from-within-tools.ipynb +++ b/docs/docs/how-tos/streaming-events-from-within-tools.ipynb @@ -196,12 +196,19 @@ "\n", "inputs = [HumanMessage(content=\"what is the weather in sf\")]\n", "final_message = \"\"\n", - "async for msg, metadata in agent.astream({\"messages\": [(\"human\", \"what items are on the shelf?\")]}, stream_mode=\"messages\"):\n", + "async for msg, metadata in agent.astream(\n", + " {\"messages\": [(\"human\", \"what items are on the shelf?\")]}, stream_mode=\"messages\"\n", + "):\n", " # Stream all messages from the tool node\n", - " if msg.content and not isinstance(msg,HumanMessage) and metadata['langgraph_node'] == 'tools' and not msg.name:\n", + " if (\n", + " msg.content\n", + " and not isinstance(msg, HumanMessage)\n", + " and metadata[\"langgraph_node\"] == \"tools\"\n", + " and not msg.name\n", + " ):\n", " print(msg.content, end=\"|\", flush=True)\n", " # Final message should come from our agent\n", - " if msg.content and metadata['langgraph_node'] == \"agent\":\n", + " if msg.content and metadata[\"langgraph_node\"] == \"agent\":\n", " final_message += msg.content" ] }, diff --git a/docs/docs/how-tos/streaming-from-final-node.ipynb b/docs/docs/how-tos/streaming-from-final-node.ipynb index 75adf2e29..bdd1e5340 100644 --- a/docs/docs/how-tos/streaming-from-final-node.ipynb +++ b/docs/docs/how-tos/streaming-from-final-node.ipynb @@ -163,6 +163,7 @@ " response.id = last_ai_message.id\n", " return {\"messages\": [response]}\n", "\n", + "\n", "workflow = StateGraph(MessagesState)\n", "\n", "workflow.add_node(\"agent\", call_model)\n", @@ -222,7 +223,8 @@ "source": [ "import warnings\n", "from langchain_core._api import LangChainBetaWarning\n", - "warnings.filterwarnings('ignore', category=LangChainBetaWarning)" + "\n", + "warnings.filterwarnings(\"ignore\", category=LangChainBetaWarning)" ] }, { @@ -260,7 +262,11 @@ "\n", "inputs = [HumanMessage(content=\"what is the weather in sf\")]\n", "async for msg, metadata in app.astream({\"messages\": inputs}, stream_mode=\"messages\"):\n", - " if msg.content and not isinstance(msg,HumanMessage) and metadata['langgraph_node'] == 'final':\n", + " if (\n", + " msg.content\n", + " and not isinstance(msg, HumanMessage)\n", + " and metadata[\"langgraph_node\"] == \"final\"\n", + " ):\n", " print(msg.content, end=\"|\", flush=True)" ] }, diff --git a/docs/docs/how-tos/streaming-subgraphs.ipynb b/docs/docs/how-tos/streaming-subgraphs.ipynb index 5d78158c1..961cb0f8e 100644 --- a/docs/docs/how-tos/streaming-subgraphs.ipynb +++ b/docs/docs/how-tos/streaming-subgraphs.ipynb @@ -87,7 +87,7 @@ "def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:\n", " if not left:\n", " left = []\n", - " \n", + "\n", " if not right:\n", " right = []\n", "\n", @@ -141,6 +141,7 @@ " # subgraph keys\n", " summary: str\n", "\n", + "\n", "def generate_summary(state: QuestionSummarizationState):\n", " docs = state[\"logs\"]\n", " # NOTE: you can implement custom summarization logic here\n", @@ -255,7 +256,7 @@ " id=\"3\",\n", " question=\"How do I create react agent in langgraph?\",\n", " answer=\"from langgraph.prebuilt import create_react_agent\",\n", - " )\n", + " ),\n", "]\n", "\n", "input = {\"raw_logs\": dummy_logs}" @@ -335,11 +336,18 @@ "source": [ "# Format the namespace slightly nicer\n", "def format_namespace(namespace):\n", - " return namespace[-1].split(':')[0]+' subgraph' if len(namespace) > 0 else 'parent graph'\n", + " return (\n", + " namespace[-1].split(\":\")[0] + \" subgraph\"\n", + " if len(namespace) > 0\n", + " else \"parent graph\"\n", + " )\n", + "\n", "\n", "for namespace, chunk in graph.stream(input, stream_mode=\"updates\", subgraphs=True):\n", " node_name = list(chunk.keys())[0]\n", - " print(f\"---------- Update from node {node_name} in {format_namespace(namespace)} ---------\")\n", + " print(\n", + " f\"---------- Update from node {node_name} in {format_namespace(namespace)} ---------\"\n", + " )\n", " print(chunk[node_name])" ] }, diff --git a/docs/docs/how-tos/streaming-tokens-without-langchain.ipynb b/docs/docs/how-tos/streaming-tokens-without-langchain.ipynb index 12843108a..14f0ef2a9 100644 --- a/docs/docs/how-tos/streaming-tokens-without-langchain.ipynb +++ b/docs/docs/how-tos/streaming-tokens-without-langchain.ipynb @@ -99,6 +99,7 @@ " ensure_config,\n", " get_callback_manager_for_config,\n", ")\n", + "\n", "openai_client = AsyncOpenAI()\n", "# define tool schema for openai tool calling\n", "\n", @@ -311,7 +312,10 @@ "from langchain_core.messages import AIMessageChunk\n", "\n", "first = True\n", - "async for msg, metadata in graph.astream({\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, stream_mode=\"messages\"):\n", + "async for msg, metadata in graph.astream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]},\n", + " stream_mode=\"messages\",\n", + "):\n", " if msg.content:\n", " print(msg.content, end=\"|\", flush=True)\n", "\n", diff --git a/docs/docs/how-tos/streaming-tokens.ipynb b/docs/docs/how-tos/streaming-tokens.ipynb index 50843d096..cba0febf8 100644 --- a/docs/docs/how-tos/streaming-tokens.ipynb +++ b/docs/docs/how-tos/streaming-tokens.ipynb @@ -339,7 +339,7 @@ " # Next, we pass in the function that will determine which node is called next.\n", " should_continue,\n", " # Next we pass in the path map - all the nodes this edge could go to\n", - " [\"tools\",END]\n", + " [\"tools\", END],\n", ")\n", "\n", "workflow.add_edge(\"tools\", \"agent\")\n", @@ -414,7 +414,7 @@ "inputs = [HumanMessage(content=\"what is the weather in sf\")]\n", "first = True\n", "async for msg, metadata in app.astream({\"messages\": inputs}, stream_mode=\"messages\"):\n", - " if msg.content and not isinstance(msg,HumanMessage):\n", + " if msg.content and not isinstance(msg, HumanMessage):\n", " print(msg.content, end=\"|\", flush=True)\n", "\n", " if isinstance(msg, AIMessageChunk):\n", diff --git a/docs/docs/how-tos/subgraph-transform-state.ipynb b/docs/docs/how-tos/subgraph-transform-state.ipynb index e1f87f540..d19fa5690 100644 --- a/docs/docs/how-tos/subgraph-transform-state.ipynb +++ b/docs/docs/how-tos/subgraph-transform-state.ipynb @@ -71,10 +71,12 @@ "class GrandChildState(TypedDict):\n", " my_grandchild_key: str\n", "\n", + "\n", "def grandchild_1(state: GrandChildState) -> GrandChildState:\n", " # NOTE: child or parent keys will not be accessible here\n", " return {\"my_grandchild_key\": state[\"my_grandchild_key\"] + \", how are you\"}\n", "\n", + "\n", "grandchild = StateGraph(GrandChildState)\n", "grandchild.add_node(\"grandchild_1\", grandchild_1)\n", "\n", @@ -194,11 +196,13 @@ "source": [ "class ParentState(TypedDict):\n", " my_key: str\n", - " \n", + "\n", + "\n", "def parent_1(state: ParentState) -> ParentState:\n", " # NOTE: child or grandchild keys won't be accessible here\n", " return {\"my_key\": \"hi \" + state[\"my_key\"]}\n", "\n", + "\n", "def parent_2(state: ParentState) -> ParentState:\n", " return {\"my_key\": state[\"my_key\"] + \" bye!\"}\n", "\n", diff --git a/docs/docs/how-tos/subgraph.ipynb b/docs/docs/how-tos/subgraph.ipynb index 3ae4a68b9..0c48a7b7d 100644 --- a/docs/docs/how-tos/subgraph.ipynb +++ b/docs/docs/how-tos/subgraph.ipynb @@ -97,7 +97,7 @@ "def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:\n", " if not left:\n", " left = []\n", - " \n", + "\n", " if not right:\n", " right = []\n", "\n", @@ -225,7 +225,7 @@ " id=\"3\",\n", " question=\"How do I create react agent in langgraph?\",\n", " answer=\"from langgraph.prebuilt import create_react_agent\",\n", - " )\n", + " ),\n", "]\n", "\n", "\n", @@ -323,6 +323,7 @@ "\n", "from typing_extensions import TypedDict\n", "\n", + "\n", "# define a simple reducer\n", "def reduce_list(left: list, right: list) -> list:\n", " if not left:\n", @@ -331,6 +332,7 @@ " right = []\n", " return left + right\n", "\n", + "\n", "# define parent and child state\n", "class ChildState(TypedDict):\n", " name: str\n", @@ -345,7 +347,7 @@ "# define a helper to build the graph\n", "def make_graph(parent_schema, child_schema):\n", " child_builder = StateGraph(child_schema)\n", - " \n", + "\n", " child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", " child_builder.add_edge(START, \"child_start\")\n", " child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", @@ -353,16 +355,16 @@ " child_builder.add_edge(\"child_start\", \"child_middle\")\n", " child_builder.add_edge(\"child_middle\", \"child_end\")\n", " child_builder.add_edge(\"child_end\", END)\n", - " \n", + "\n", " builder = StateGraph(parent_schema)\n", - " \n", + "\n", " builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", " builder.add_edge(START, \"grandparent\")\n", " builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", " builder.add_node(\"child\", child_builder.compile())\n", " builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", " builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", - " \n", + "\n", " # Add connections\n", " builder.add_edge(\"grandparent\", \"parent\")\n", " builder.add_edge(\"parent\", \"child\")\n", @@ -508,6 +510,7 @@ "source": [ "import uuid\n", "\n", + "\n", "def reduce_list(left: list | None, right: list | None) -> list:\n", " \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n", " if not left:\n", diff --git a/docs/docs/how-tos/subgraphs-manage-state.ipynb b/docs/docs/how-tos/subgraphs-manage-state.ipynb index 0da0afdc5..a7530ca34 100644 --- a/docs/docs/how-tos/subgraphs-manage-state.ipynb +++ b/docs/docs/how-tos/subgraphs-manage-state.ipynb @@ -89,6 +89,7 @@ " \"\"\"Get the weather for a specific city\"\"\"\n", " return f\"It's sunny in {city}!\"\n", "\n", + "\n", "raw_model = ChatOpenAI()\n", "model = raw_model.with_structured_output(get_weather)\n", "\n", @@ -98,11 +99,12 @@ "\n", "\n", "def model_node(state: SubGraphState):\n", - " result = model.invoke(state['messages'])\n", + " result = model.invoke(state[\"messages\"])\n", " return {\"city\": result[\"city\"]}\n", "\n", + "\n", "def weather_node(state: SubGraphState):\n", - " result = get_weather.invoke({\"city\": state['city']})\n", + " result = get_weather.invoke({\"city\": state[\"city\"]})\n", " return {\"messages\": [{\"role\": \"assistant\", \"content\": result}]}\n", "\n", "\n", @@ -145,22 +147,26 @@ "class Router(TypedDict):\n", " route: Literal[\"weather\", \"other\"]\n", "\n", + "\n", "router_model = raw_model.with_structured_output(Router)\n", - " \n", + "\n", + "\n", "def router_node(state: RouterState):\n", " system_message = \"Classify the incoming query as either about weather or not.\"\n", - " messages = [{\"role\": \"system\", \"content\": system_message}] + state['messages']\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + state[\"messages\"]\n", " route = router_model.invoke(messages)\n", - " return {\"route\": route['route']}\n", + " return {\"route\": route[\"route\"]}\n", "\n", "\n", "def normal_llm_node(state: RouterState):\n", - " response = raw_model.invoke(state['messages'])\n", + " response = raw_model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]}\n", "\n", "\n", - "def route_after_prediction(state: RouterState) -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n", - " if state['route'] == \"weather\":\n", + "def route_after_prediction(\n", + " state: RouterState,\n", + ") -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n", + " if state[\"route\"] == \"weather\":\n", " return \"weather_graph\"\n", " else:\n", " return \"normal_llm_node\"\n", @@ -421,7 +427,9 @@ "metadata": {}, "outputs": [], "source": [ - "parent_graph_state_before_subgraph = next(h for h in graph.get_state_history(config) if h.next == ('weather_graph',))" + "parent_graph_state_before_subgraph = next(\n", + " h for h in graph.get_state_history(config) if h.next == (\"weather_graph\",)\n", + ")" ] }, { @@ -430,7 +438,11 @@ "metadata": {}, "outputs": [], "source": [ - "subgraph_state_before_model_node = next(h for h in graph.get_state_history(parent_graph_state_before_subgraph.tasks[0].state) if h.next == ('model_node',))\n", + "subgraph_state_before_model_node = next(\n", + " h\n", + " for h in graph.get_state_history(parent_graph_state_before_subgraph.tasks[0].state)\n", + " if h.next == (\"model_node\",)\n", + ")\n", "\n", "# This pattern can be extended no matter how many levels deep - image model node was another subgraph in this case\n", "# subsubgraph_stat_history = next(h for h in graph.get_state_history(subgraph_state_before_model_node.tasks[0].state) if h.next == ('my_subsubgraph_node',))" @@ -486,7 +498,12 @@ } ], "source": [ - "for value in graph.stream(None, config=subgraph_state_before_model_node.config, stream_mode=\"values\", subgraphs=True):\n", + "for value in graph.stream(\n", + " None,\n", + " config=subgraph_state_before_model_node.config,\n", + " stream_mode=\"values\",\n", + " subgraphs=True,\n", + "):\n", " print(value)" ] }, @@ -546,7 +563,7 @@ ], "source": [ "state = graph.get_state(config, subgraphs=True)\n", - "state.values['messages']" + "state.values[\"messages\"]" ] }, { @@ -637,16 +654,22 @@ "source": [ "config = {\"configurable\": {\"thread_id\": \"14\"}}\n", "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", - "for update in graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + "for update in graph.stream(\n", + " inputs, config=config, stream_mode=\"updates\", subgraphs=True\n", + "):\n", " print(update)\n", "# Graph execution should stop before the weather node\n", "print(\"interrupted!\")\n", "state = graph.get_state(config, subgraphs=True)\n", "# We update the state by passing in the message we want returned from the weather node, and make sure to use as_node\n", - "graph.update_state(state.tasks[0].state.config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_node\")\n", + "graph.update_state(\n", + " state.tasks[0].state.config,\n", + " {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]},\n", + " as_node=\"weather_node\",\n", + ")\n", "for update in graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n", " print(update)\n", - "print(graph.get_state(config).values['messages'])" + "print(graph.get_state(config).values[\"messages\"])" ] }, { @@ -679,16 +702,22 @@ "source": [ "config = {\"configurable\": {\"thread_id\": \"8\"}}\n", "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", - "for update in graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + "for update in graph.stream(\n", + " inputs, config=config, stream_mode=\"updates\", subgraphs=True\n", + "):\n", " print(update)\n", "# Graph execution should stop before the weather node\n", "print(\"interrupted!\")\n", "# We update the state by passing in the message we want returned from the weather graph, making sure to use as_node\n", "# Note that we don't need to pass in the subgraph config, since we aren't updating the state inside the subgraph\n", - "graph.update_state(config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_graph\")\n", + "graph.update_state(\n", + " config,\n", + " {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]},\n", + " as_node=\"weather_graph\",\n", + ")\n", "for update in graph.stream(None, config=config, stream_mode=\"updates\"):\n", " print(update)\n", - "print(graph.get_state(config).values['messages'])" + "print(graph.get_state(config).values[\"messages\"])" ] }, { @@ -723,22 +752,26 @@ "class Router(TypedDict):\n", " route: Literal[\"weather\", \"other\"]\n", "\n", + "\n", "router_model = raw_model.with_structured_output(Router)\n", - " \n", + "\n", + "\n", "def router_node(state: RouterState):\n", " system_message = \"Classify the incoming query as either about weather or not.\"\n", - " messages = [{\"role\": \"system\", \"content\": system_message}] + state['messages']\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + state[\"messages\"]\n", " route = router_model.invoke(messages)\n", - " return {\"route\": route['route']}\n", + " return {\"route\": route[\"route\"]}\n", "\n", "\n", "def normal_llm_node(state: RouterState):\n", - " response = raw_model.invoke(state['messages'])\n", + " response = raw_model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]}\n", "\n", "\n", - "def route_after_prediction(state: RouterState) -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n", - " if state['route'] == \"weather\":\n", + "def route_after_prediction(\n", + " state: RouterState,\n", + ") -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n", + " if state[\"route\"] == \"weather\":\n", " return \"weather_graph\"\n", " else:\n", " return \"normal_llm_node\"\n", @@ -765,24 +798,30 @@ "\n", "memory = MemorySaver()\n", "\n", + "\n", "class GrandfatherState(MessagesState):\n", " to_continue: bool\n", - " \n", + "\n", + "\n", "def router_node(state: GrandfatherState):\n", " # Dummy logic that will always continue\n", " return {\"to_continue\": True}\n", "\n", + "\n", "def route_after_prediction(state: GrandfatherState):\n", - " if state['to_continue']:\n", + " if state[\"to_continue\"]:\n", " return \"graph\"\n", " else:\n", " return END\n", "\n", + "\n", "grandparent_graph = StateGraph(GrandfatherState)\n", "grandparent_graph.add_node(router_node)\n", "grandparent_graph.add_node(\"graph\", graph)\n", "grandparent_graph.add_edge(START, \"router_node\")\n", - "grandparent_graph.add_conditional_edges(\"router_node\", route_after_prediction, ['graph',END])\n", + "grandparent_graph.add_conditional_edges(\n", + " \"router_node\", route_after_prediction, [\"graph\", END]\n", + ")\n", "grandparent_graph.add_edge(\"graph\", END)\n", "grandparent_graph = grandparent_graph.compile(checkpointer=MemorySaver())" ] @@ -835,7 +874,9 @@ "source": [ "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", - "for update in grandparent_graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + "for update in grandparent_graph.stream(\n", + " inputs, config=config, stream_mode=\"updates\", subgraphs=True\n", + "):\n", " print(update)" ] }, @@ -897,10 +938,16 @@ "grandparent_graph_state = state\n", "parent_graph_state = grandparent_graph_state.tasks[0].state\n", "subgraph_state = parent_graph_state.tasks[0].state\n", - "grandparent_graph.update_state(subgraph_state.config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_node\")\n", - "for update in grandparent_graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n", + "grandparent_graph.update_state(\n", + " subgraph_state.config,\n", + " {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]},\n", + " as_node=\"weather_node\",\n", + ")\n", + "for update in grandparent_graph.stream(\n", + " None, config=config, stream_mode=\"updates\", subgraphs=True\n", + "):\n", " print(update)\n", - "print(grandparent_graph.get_state(config).values['messages'])" + "print(grandparent_graph.get_state(config).values[\"messages\"])" ] }, { diff --git a/docs/docs/how-tos/tool-calling-errors.ipynb b/docs/docs/how-tos/tool-calling-errors.ipynb index 0ff1809eb..f8d8e3881 100644 --- a/docs/docs/how-tos/tool-calling-errors.ipynb +++ b/docs/docs/how-tos/tool-calling-errors.ipynb @@ -155,11 +155,7 @@ "workflow.add_node(\"tools\", tool_node)\n", "\n", "workflow.add_edge(START, \"agent\")\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " should_continue,\n", - " ['tools',END]\n", - ")\n", + "workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n", "workflow.add_edge(\"tools\", \"agent\")\n", "\n", "app = workflow.compile()" @@ -296,6 +292,7 @@ "from langchain_core.output_parsers import StrOutputParser\n", "from pydantic import BaseModel, Field\n", "\n", + "\n", "class HaikuRequest(BaseModel):\n", " topic: list[str] = Field(\n", " max_length=3,\n", @@ -303,7 +300,6 @@ " )\n", "\n", "\n", - "\n", "@tool\n", "def master_haiku_generator(request: HaikuRequest):\n", " \"\"\"Generates a haiku based on the provided topics.\"\"\"\n", @@ -341,11 +337,7 @@ "workflow.add_node(\"tools\", tool_node)\n", "\n", "workflow.add_edge(START, \"agent\")\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " should_continue,\n", - " ['tools',END]\n", - ")\n", + "workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n", "workflow.add_edge(\"tools\", \"agent\")\n", "\n", "app = workflow.compile()\n", @@ -483,11 +475,7 @@ "workflow.add_node(\"fallback_agent\", call_fallback_model)\n", "\n", "workflow.add_edge(START, \"agent\")\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " should_continue,\n", - " ['tools',END]\n", - ")\n", + "workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n", "workflow.add_conditional_edges(\"tools\", should_fallback)\n", "workflow.add_edge(\"remove_failed_tool_call_attempt\", \"fallback_agent\")\n", "workflow.add_edge(\"fallback_agent\", \"tools\")\n", diff --git a/docs/docs/how-tos/tool-calling.ipynb b/docs/docs/how-tos/tool-calling.ipynb index f895915f0..aab12d02f 100644 --- a/docs/docs/how-tos/tool-calling.ipynb +++ b/docs/docs/how-tos/tool-calling.ipynb @@ -335,11 +335,7 @@ "workflow.add_node(\"tools\", tool_node)\n", "\n", "workflow.add_edge(START, \"agent\")\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " should_continue,\n", - " ['tools',END]\n", - ")\n", + "workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n", "workflow.add_edge(\"tools\", \"agent\")\n", "\n", "app = workflow.compile()" diff --git a/docs/docs/how-tos/visualization.ipynb b/docs/docs/how-tos/visualization.ipynb index 4917355b0..1a112863b 100644 --- a/docs/docs/how-tos/visualization.ipynb +++ b/docs/docs/how-tos/visualization.ipynb @@ -425,7 +425,9 @@ "try:\n", " display(Image(app.get_graph().draw_png()))\n", "except ImportError:\n", - " print(\"You likely need to install dependencies for pygraphviz, see more here https://github.com/pygraphviz/pygraphviz/blob/main/INSTALL.txt\")" + " print(\n", + " \"You likely need to install dependencies for pygraphviz, see more here https://github.com/pygraphviz/pygraphviz/blob/main/INSTALL.txt\"\n", + " )" ] } ], diff --git a/docs/docs/tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb b/docs/docs/tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb index c7be76d09..e086a18b1 100644 --- a/docs/docs/tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb +++ b/docs/docs/tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb @@ -407,7 +407,7 @@ } ], "source": [ - "for chunk in simulation.stream({\"messages\":[]}):\n", + "for chunk in simulation.stream({\"messages\": []}):\n", " # Print out all events aside from the final end chunk\n", " if END not in chunk:\n", " print(chunk)\n", diff --git a/docs/docs/tutorials/chatbots/information-gather-prompting.ipynb b/docs/docs/tutorials/chatbots/information-gather-prompting.ipynb index 03e253513..4f186ec05 100644 --- a/docs/docs/tutorials/chatbots/information-gather-prompting.ipynb +++ b/docs/docs/tutorials/chatbots/information-gather-prompting.ipynb @@ -378,8 +378,8 @@ ], "source": [ "import uuid\n", - " \n", - "cached_human_responses = ['hi!','rag prompt','1 rag, 2 none, 3 no, 4 no','red','q']\n", + "\n", + "cached_human_responses = [\"hi!\", \"rag prompt\", \"1 rag, 2 none, 3 no, 4 no\", \"red\", \"q\"]\n", "cached_response_index = 0\n", "config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n", "while True:\n", diff --git a/docs/docs/tutorials/code_assistant/langgraph_code_assistant.ipynb b/docs/docs/tutorials/code_assistant/langgraph_code_assistant.ipynb index 9fc176c41..788a8c1e1 100644 --- a/docs/docs/tutorials/code_assistant/langgraph_code_assistant.ipynb +++ b/docs/docs/tutorials/code_assistant/langgraph_code_assistant.ipynb @@ -196,7 +196,9 @@ "llm = ChatOpenAI(temperature=0, model=expt_llm)\n", "code_gen_chain_oai = code_gen_prompt | llm.with_structured_output(code)\n", "question = \"How do I build a RAG chain in LCEL?\"\n", - "solution = code_gen_chain_oai.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})\n", + "solution = code_gen_chain_oai.invoke(\n", + " {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n", + ")\n", "solution" ] }, @@ -618,7 +620,7 @@ ], "source": [ "question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\n", - "solution = app.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0, \"error\":\"\"})" + "solution = app.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0, \"error\": \"\"})" ] }, { @@ -639,7 +641,7 @@ } ], "source": [ - "solution['generation']" + "solution[\"generation\"]" ] }, { @@ -764,7 +766,9 @@ "\n", "def predict_langgraph(example: dict):\n", " \"\"\"LangGraph\"\"\"\n", - " graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0, \"error\": \"\"})\n", + " graph = app.invoke(\n", + " {\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0, \"error\": \"\"}\n", + " )\n", " solution = graph[\"generation\"]\n", " return {\"imports\": solution.imports, \"code\": solution.code}" ] diff --git a/docs/docs/tutorials/customer-support/customer-support.ipynb b/docs/docs/tutorials/customer-support/customer-support.ipynb index 26c7fc510..6dc68cb23 100644 --- a/docs/docs/tutorials/customer-support/customer-support.ipynb +++ b/docs/docs/tutorials/customer-support/customer-support.ipynb @@ -107,6 +107,8 @@ " f.write(response.content)\n", " # Backup - we will use this to \"reset\" our DB in each section\n", " shutil.copy(local_file, backup_file)\n", + "\n", + "\n", "# Convert the flights to present time for our tutorial\n", "def update_dates(file):\n", " shutil.copy(backup_file, file)\n", @@ -151,6 +153,7 @@ "\n", " return file\n", "\n", + "\n", "db = update_dates(local_file)" ] }, @@ -2560,9 +2563,7 @@ "\n", "\n", "builder.add_conditional_edges(\n", - " \"assistant\",\n", - " route_tools,\n", - " [\"safe_tools\", \"sensitive_tools\", END]\n", + " \"assistant\", route_tools, [\"safe_tools\", \"sensitive_tools\", END]\n", ")\n", "builder.add_edge(\"safe_tools\", \"assistant\")\n", "builder.add_edge(\"sensitive_tools\", \"assistant\")\n", @@ -3540,7 +3541,11 @@ "\n", "builder.add_edge(\"update_flight_sensitive_tools\", \"update_flight\")\n", "builder.add_edge(\"update_flight_safe_tools\", \"update_flight\")\n", - "builder.add_conditional_edges(\"update_flight\", route_update_flight, [\"update_flight_sensitive_tools\",\"update_flight_safe_tools\",\"leave_skill\",END])\n", + "builder.add_conditional_edges(\n", + " \"update_flight\",\n", + " route_update_flight,\n", + " [\"update_flight_sensitive_tools\", \"update_flight_safe_tools\", \"leave_skill\", END],\n", + ")\n", "\n", "\n", "# This node will be shared for exiting all specialized assistants\n", @@ -3620,7 +3625,16 @@ "\n", "builder.add_edge(\"book_car_rental_sensitive_tools\", \"book_car_rental\")\n", "builder.add_edge(\"book_car_rental_safe_tools\", \"book_car_rental\")\n", - "builder.add_conditional_edges(\"book_car_rental\", route_book_car_rental, [\"book_car_rental_safe_tools\",\"book_car_rental_sensitive_tools\",\"leave_skill\",END])" + "builder.add_conditional_edges(\n", + " \"book_car_rental\",\n", + " route_book_car_rental,\n", + " [\n", + " \"book_car_rental_safe_tools\",\n", + " \"book_car_rental_sensitive_tools\",\n", + " \"leave_skill\",\n", + " END,\n", + " ],\n", + ")" ] }, { @@ -3672,7 +3686,11 @@ "\n", "builder.add_edge(\"book_hotel_sensitive_tools\", \"book_hotel\")\n", "builder.add_edge(\"book_hotel_safe_tools\", \"book_hotel\")\n", - "builder.add_conditional_edges(\"book_hotel\", route_book_hotel, [\"leave_skill\", \"book_hotel_safe_tools\", \"book_hotel_sensitive_tools\", END])" + "builder.add_conditional_edges(\n", + " \"book_hotel\",\n", + " route_book_hotel,\n", + " [\"leave_skill\", \"book_hotel_safe_tools\", \"book_hotel_sensitive_tools\", END],\n", + ")" ] }, { @@ -3725,7 +3743,11 @@ "\n", "builder.add_edge(\"book_excursion_sensitive_tools\", \"book_excursion\")\n", "builder.add_edge(\"book_excursion_safe_tools\", \"book_excursion\")\n", - "builder.add_conditional_edges(\"book_excursion\", route_book_excursion, [\"book_excursion_safe_tools\",\"book_excursion_sensitive_tools\",\"leave_skill\",END])" + "builder.add_conditional_edges(\n", + " \"book_excursion\",\n", + " route_book_excursion,\n", + " [\"book_excursion_safe_tools\", \"book_excursion_sensitive_tools\", \"leave_skill\", END],\n", + ")" ] }, { diff --git a/docs/docs/tutorials/extraction/retries.ipynb b/docs/docs/tutorials/extraction/retries.ipynb index 23e5eef9f..6f9da9114 100644 --- a/docs/docs/tutorials/extraction/retries.ipynb +++ b/docs/docs/tutorials/extraction/retries.ipynb @@ -291,7 +291,7 @@ " return \"validator\"\n", " return END\n", "\n", - " builder.add_conditional_edges(\"llm\", route_validator, ['validator',END])\n", + " builder.add_conditional_edges(\"llm\", route_validator, [\"validator\", END])\n", " builder.add_edge(\"fallback\", \"validator\")\n", " max_attempts = retry_strategy.get(\"max_attempts\", 3)\n", "\n", @@ -307,7 +307,9 @@ " return \"fallback\"\n", " return \"finalizer\"\n", "\n", - " builder.add_conditional_edges(\"validator\", route_validation, [\"finalizer\", \"fallback\"])\n", + " builder.add_conditional_edges(\n", + " \"validator\", route_validation, [\"finalizer\", \"fallback\"]\n", + " )\n", "\n", " builder.add_edge(\"finalizer\", END)\n", "\n", diff --git a/docs/docs/tutorials/introduction.ipynb b/docs/docs/tutorials/introduction.ipynb index 4707fe311..400fe1251 100644 --- a/docs/docs/tutorials/introduction.ipynb +++ b/docs/docs/tutorials/introduction.ipynb @@ -2108,6 +2108,7 @@ "source": [ "from pydantic import BaseModel\n", "\n", + "\n", "class RequestAssistance(BaseModel):\n", " \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n", "\n", @@ -2668,6 +2669,7 @@ "from langchain_anthropic import ChatAnthropic\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", "from langchain_core.messages import AIMessage, ToolMessage\n", + "\n", "# NOTE: you must use langchain-core >= 0.3 with Pydantic v2\n", "from pydantic import BaseModel\n", "from typing_extensions import TypedDict\n", diff --git a/docs/docs/tutorials/lats/lats.ipynb b/docs/docs/tutorials/lats/lats.ipynb index ca3f1b7be..0c7cc08d3 100644 --- a/docs/docs/tutorials/lats/lats.ipynb +++ b/docs/docs/tutorials/lats/lats.ipynb @@ -142,6 +142,7 @@ " def normalized_score(self) -> float:\n", " return self.score / 10.0\n", "\n", + "\n", "class Node:\n", " def __init__(\n", " self,\n", @@ -476,12 +477,22 @@ " \"\"\"Generate the initial candidate response.\"\"\"\n", " res = initial_answer_chain.invoke({\"input\": state[\"input\"]})\n", " parsed = parser.invoke(res)\n", - " tool_responses = [tool_node.invoke(\n", - " {\"messages\": [\n", - " AIMessage(content=\"\",tool_calls=[{\"name\":r[\"type\"], \"args\":r[\"args\"], 'id':r['id']}]) \n", - " ]}\n", - " ) for r in parsed]\n", - " output_messages = [res] + [tr['messages'][0] for tr in tool_responses]\n", + " tool_responses = [\n", + " tool_node.invoke(\n", + " {\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=\"\",\n", + " tool_calls=[\n", + " {\"name\": r[\"type\"], \"args\": r[\"args\"], \"id\": r[\"id\"]}\n", + " ],\n", + " )\n", + " ]\n", + " }\n", + " )\n", + " for r in parsed\n", + " ]\n", + " output_messages = [res] + [tr[\"messages\"][0] for tr in tool_responses]\n", " reflection = reflection_chain.invoke(\n", " {\"input\": state[\"input\"], \"candidate\": output_messages}\n", " )\n", @@ -575,12 +586,13 @@ "source": [ "from collections import defaultdict\n", "\n", + "\n", "def select(root: Node) -> dict:\n", " \"\"\"Starting from the root node a child node is selected at each tree level until a leaf node is reached.\"\"\"\n", "\n", " if not root.children:\n", " return root\n", - " \n", + "\n", " node = root\n", " while node.children:\n", " max_child = max(node.children, key=lambda child: child.upper_confidence_bound())\n", @@ -588,6 +600,7 @@ "\n", " return node\n", "\n", + "\n", "def expand(state: TreeState, config: RunnableConfig) -> dict:\n", " \"\"\"Starting from the \"best\" node in the tree, generate N candidates for the next step.\"\"\"\n", " root = state[\"root\"]\n", @@ -603,16 +616,31 @@ " for i, tool_calls in enumerate(parsed)\n", " for tool_call in tool_calls\n", " ]\n", - " tool_responses = [(i,tool_node.invoke(\n", - " {\"messages\":\n", - " [AIMessage(content=\"\",tool_calls=[{\"name\":tool_call[\"type\"], \"args\":tool_call[\"args\"], 'id':tool_call['id']}])]\n", - " }\n", - " )) for i, tool_call in flattened]\n", + " tool_responses = [\n", + " (\n", + " i,\n", + " tool_node.invoke(\n", + " {\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=\"\",\n", + " tool_calls=[\n", + " {\n", + " \"name\": tool_call[\"type\"],\n", + " \"args\": tool_call[\"args\"],\n", + " \"id\": tool_call[\"id\"],\n", + " }\n", + " ],\n", + " )\n", + " ]\n", + " }\n", + " ),\n", + " )\n", + " for i, tool_call in flattened\n", + " ]\n", " collected_responses = defaultdict(list)\n", " for i, resp in tool_responses:\n", - " collected_responses[i].append(\n", - " resp['messages'][0]\n", - " )\n", + " collected_responses[i].append(resp[\"messages\"][0])\n", " output_messages = []\n", " for i, candidate in enumerate(new_candidates):\n", " output_messages.append([candidate] + collected_responses[i])\n", @@ -675,13 +703,13 @@ " \"start\",\n", " # Either expand/rollout or finish\n", " should_loop,\n", - " ['expand',END]\n", + " [\"expand\", END],\n", ")\n", "builder.add_conditional_edges(\n", " \"expand\",\n", " # Either continue to rollout or finish\n", " should_loop,\n", - " ['expand',END]\n", + " [\"expand\", END],\n", ")\n", "\n", "graph = builder.compile()" diff --git a/docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb b/docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb index cc57cdc3a..d39210c6b 100644 --- a/docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb +++ b/docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb @@ -859,8 +859,7 @@ " args_for_tasks[task[\"idx\"]] = task[\"args\"]\n", " if (\n", " # Depends on other tasks\n", - " deps\n", - " and (any([dep not in observations for dep in deps]))\n", + " deps and (any([dep not in observations for dep in deps]))\n", " ):\n", " futures.append(\n", " executor.submit(\n", @@ -883,7 +882,10 @@ " }\n", " tool_messages = [\n", " FunctionMessage(\n", - " name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}, tool_call_id = k\n", + " name=name,\n", + " content=str(obs),\n", + " additional_kwargs={\"idx\": k, \"args\": task_args},\n", + " tool_call_id=k,\n", " )\n", " for k, (name, task_args, obs) in new_observations.items()\n", " ]\n", @@ -936,7 +938,9 @@ "metadata": {}, "outputs": [], "source": [ - "tool_messages = plan_and_schedule.invoke({\"messages\":[HumanMessage(content=example_question)]})['messages']" + "tool_messages = plan_and_schedule.invoke(\n", + " {\"messages\": [HumanMessage(content=example_question)]}\n", + ")[\"messages\"]" ] }, { @@ -1050,11 +1054,13 @@ "def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n", " response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n", " if isinstance(decision.action, Replan):\n", - " return {\"messages\": response + [\n", - " SystemMessage(\n", - " content=f\"Context from last attempt: {decision.action.feedback}\"\n", - " )\n", - " ]\n", + " return {\n", + " \"messages\": response\n", + " + [\n", + " SystemMessage(\n", + " content=f\"Context from last attempt: {decision.action.feedback}\"\n", + " )\n", + " ]\n", " }\n", " else:\n", " return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n", @@ -1102,7 +1108,7 @@ } ], "source": [ - "joiner.invoke({\"messages\":input_messages})" + "joiner.invoke({\"messages\": input_messages})" ] }, { @@ -1217,7 +1223,7 @@ ], "source": [ "# Final answer\n", - "print(step['join']['messages'][-1].content)" + "print(step[\"join\"][\"messages\"][-1].content)" ] }, { @@ -1248,12 +1254,13 @@ } ], "source": [ - "steps = chain.stream({\"messages\":\n", - " [\n", - " HumanMessage(\n", - " content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n", - " )\n", - " ]\n", + "steps = chain.stream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n", + " )\n", + " ]\n", " },\n", " {\n", " \"recursion_limit\": 100,\n", @@ -1280,7 +1287,7 @@ ], "source": [ "# Final answer\n", - "print(step['join']['messages'][-1].content)" + "print(step[\"join\"][\"messages\"][-1].content)" ] }, { @@ -1307,12 +1314,14 @@ } ], "source": [ - "for step in chain.stream({\"messages\":\n", - " [\n", - " HumanMessage(\n", - " content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n", - " )\n", - " ]}\n", + "for step in chain.stream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n", + " )\n", + " ]\n", + " }\n", "):\n", " print(step)" ] @@ -1335,7 +1344,7 @@ ], "source": [ "# Final answer\n", - "print(step['join']['messages'][-1].content)" + "print(step[\"join\"][\"messages\"][-1].content)" ] }, { @@ -1364,12 +1373,14 @@ } ], "source": [ - "for step in chain.stream({\"messages\":\n", - " [\n", - " HumanMessage(\n", - " content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n", - " )\n", - " ]}\n", + "for step in chain.stream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n", + " )\n", + " ]\n", + " }\n", "):\n", " print(step)" ] diff --git a/docs/docs/tutorials/memory/long_term_memory_agent.ipynb b/docs/docs/tutorials/memory/long_term_memory_agent.ipynb index 1f4e33c98..673e8152c 100644 --- a/docs/docs/tutorials/memory/long_term_memory_agent.ipynb +++ b/docs/docs/tutorials/memory/long_term_memory_agent.ipynb @@ -158,7 +158,7 @@ " user_id = config[\"configurable\"].get(\"user_id\")\n", " if user_id is None:\n", " raise ValueError(\"User ID needs to be provided to save a memory.\")\n", - " \n", + "\n", " return user_id\n", "\n", "\n", @@ -166,7 +166,9 @@ "def save_recall_memory(memory: str, config: RunnableConfig) -> str:\n", " \"\"\"Save memory to vectorstore for later semantic retrieval.\"\"\"\n", " user_id = get_user_id(config)\n", - " document = Document(page_content=memory, id=str(uuid.uuid4()), metadata={\"user_id\": user_id})\n", + " document = Document(\n", + " page_content=memory, id=str(uuid.uuid4()), metadata={\"user_id\": user_id}\n", + " )\n", " recall_vector_store.add_documents([document])\n", " return memory\n", "\n", @@ -175,10 +177,13 @@ "def search_recall_memories(query: str, config: RunnableConfig) -> List[str]:\n", " \"\"\"Search for relevant memories.\"\"\"\n", " user_id = get_user_id(config)\n", + "\n", " def _filter_function(doc: Document) -> bool:\n", " return doc.metadata.get(\"user_id\") == user_id\n", "\n", - " documents = recall_vector_store.similarity_search(query, k=3, filter=_filter_function)\n", + " documents = recall_vector_store.similarity_search(\n", + " query, k=3, filter=_filter_function\n", + " )\n", " return [document.page_content for document in documents]" ] }, @@ -281,7 +286,7 @@ " \" information you want to retain in the next conversation. If you\"\n", " \" do call tools, all text preceding the tool call is an internal\"\n", " \" message. Respond AFTER calling the tool, once you have\"\n", - " \" confirmation that the tool completed successfully.\\n\\n\"\n", + " \" confirmation that the tool completed successfully.\\n\\n\",\n", " ),\n", " (\"placeholder\", \"{messages}\"),\n", " ]\n", @@ -300,6 +305,7 @@ "\n", "tokenizer = tiktoken.encoding_for_model(\"gpt-4o\")\n", "\n", + "\n", "def agent(state: State) -> State:\n", " \"\"\"Process the current state and generate a response using the LLM.\n", "\n", @@ -354,7 +360,7 @@ " msg = state[\"messages\"][-1]\n", " if msg.tool_calls:\n", " return \"tools\"\n", - " \n", + "\n", " return END" ] }, @@ -594,7 +600,10 @@ } ], "source": [ - "for chunk in graph.stream({\"messages\": [(\"user\", \"yes -- pepperoni!\")]}, config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}}):\n", + "for chunk in graph.stream(\n", + " {\"messages\": [(\"user\", \"yes -- pepperoni!\")]},\n", + " config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}},\n", + "):\n", " pretty_print_stream_chunk(chunk)" ] }, @@ -638,7 +647,10 @@ } ], "source": [ - "for chunk in graph.stream({\"messages\": [(\"user\", \"i also just moved to new york\")]}, config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}}):\n", + "for chunk in graph.stream(\n", + " {\"messages\": [(\"user\", \"i also just moved to new york\")]},\n", + " config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}},\n", + "):\n", " pretty_print_stream_chunk(chunk)" ] }, @@ -682,7 +694,9 @@ "source": [ "config = {\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"2\"}}\n", "\n", - "for chunk in graph.stream({\"messages\": [(\"user\", \"where should i go for dinner?\")]}, config=config):\n", + "for chunk in graph.stream(\n", + " {\"messages\": [(\"user\", \"where should i go for dinner?\")]}, config=config\n", + "):\n", " pretty_print_stream_chunk(chunk)" ] }, @@ -740,7 +754,10 @@ } ], "source": [ - "for chunk in graph.stream({\"messages\": [(\"user\", \"what's the address for joe's in greenwich village?\")]}, config=config):\n", + "for chunk in graph.stream(\n", + " {\"messages\": [(\"user\", \"what's the address for joe's in greenwich village?\")]},\n", + " config=config,\n", + "):\n", " pretty_print_stream_chunk(chunk)" ] }, @@ -924,7 +941,9 @@ } ], "source": [ - "for chunk in graph.stream({\"messages\": [(\"user\", \"My friend John likes Pizza.\")]}, config=config):\n", + "for chunk in graph.stream(\n", + " {\"messages\": [(\"user\", \"My friend John likes Pizza.\")]}, config=config\n", + "):\n", " pretty_print_stream_chunk(chunk)" ] }, @@ -962,7 +981,9 @@ "source": [ "config = {\"configurable\": {\"user_id\": \"3\", \"thread_id\": \"2\"}}\n", "\n", - "for chunk in graph.stream({\"messages\": [(\"user\", \"What food should I bring to John's party?\")]}, config=config):\n", + "for chunk in graph.stream(\n", + " {\"messages\": [(\"user\", \"What food should I bring to John's party?\")]}, config=config\n", + "):\n", " pretty_print_stream_chunk(chunk)" ] }, @@ -1007,7 +1028,9 @@ "\n", "\n", "# Fetch records\n", - "records = recall_vector_store.similarity_search(\"Alice\", k=2, filter=lambda doc: doc.metadata[\"user_id\"] == \"3\")\n", + "records = recall_vector_store.similarity_search(\n", + " \"Alice\", k=2, filter=lambda doc: doc.metadata[\"user_id\"] == \"3\"\n", + ")\n", "\n", "\n", "# Plot graph\n", diff --git a/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb b/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb index 739809bd4..667b541b6 100644 --- a/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb +++ b/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb @@ -124,9 +124,12 @@ "source": [ "from langchain_core.messages import HumanMessage\n", "\n", + "\n", "def agent_node(state, agent, name):\n", " result = agent.invoke(state)\n", - " return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}" + " return {\n", + " \"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]\n", + " }" ] }, { @@ -163,9 +166,11 @@ "# and decides when the work is completed\n", "options = [\"FINISH\"] + members\n", "\n", + "\n", "class routeResponse(BaseModel):\n", " next: Literal[*options]\n", "\n", + "\n", "prompt = ChatPromptTemplate.from_messages(\n", " [\n", " (\"system\", system_prompt),\n", @@ -181,11 +186,9 @@ "\n", "llm = ChatOpenAI(model=\"gpt-4o\")\n", "\n", + "\n", "def supervisor_agent(state):\n", - " supervisor_chain = (\n", - " prompt\n", - " | llm.with_structured_output(routeResponse)\n", - " )\n", + " supervisor_chain = prompt | llm.with_structured_output(routeResponse)\n", " return supervisor_chain.invoke(state)" ] }, @@ -216,6 +219,7 @@ "from langgraph.graph import END, StateGraph, START\n", "from langgraph.prebuilt import create_react_agent\n", "\n", + "\n", "# The agent state is the input to each node in the graph\n", "class AgentState(TypedDict):\n", " # The annotation tells the graph that new messages will always\n", diff --git a/docs/docs/tutorials/multi_agent/hierarchical_agent_teams.ipynb b/docs/docs/tutorials/multi_agent/hierarchical_agent_teams.ipynb index b4590d4f8..3aefca508 100644 --- a/docs/docs/tutorials/multi_agent/hierarchical_agent_teams.ipynb +++ b/docs/docs/tutorials/multi_agent/hierarchical_agent_teams.ipynb @@ -305,7 +305,9 @@ "\n", "def agent_node(state, agent, name):\n", " result = agent.invoke(state)\n", - " return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}\n", + " return {\n", + " \"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]\n", + " }\n", "\n", "\n", "def create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n", @@ -340,7 +342,8 @@ " ]\n", " ).partial(options=str(options), team_members=\", \".join(members))\n", " return (\n", - " prompt | trimmer\n", + " prompt\n", + " | trimmer\n", " | llm.bind_functions(functions=[function_def], function_call=\"route\")\n", " | JsonOutputFunctionsParser()\n", " )" @@ -379,6 +382,7 @@ "from langchain_openai.chat_models import ChatOpenAI\n", "from langgraph.prebuilt import create_react_agent\n", "\n", + "\n", "# ResearchTeam graph state\n", "class ResearchTeamState(TypedDict):\n", " # A message is added after each team member finishes\n", @@ -595,14 +599,16 @@ "\n", "llm = ChatOpenAI(model=\"gpt-4o\")\n", "\n", - "doc_writer_agent = create_react_agent(llm, tools=[write_document, edit_document, read_document])\n", + "doc_writer_agent = create_react_agent(\n", + " llm, tools=[write_document, edit_document, read_document]\n", + ")\n", "# Injects current directory working state before each call\n", "context_aware_doc_writer_agent = prelude | doc_writer_agent\n", "doc_writing_node = functools.partial(\n", " agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n", ")\n", "\n", - "note_taking_agent = create_react_agent(llm,tools=[create_outline, read_document])\n", + "note_taking_agent = create_react_agent(llm, tools=[create_outline, read_document])\n", "context_aware_note_taking_agent = prelude | note_taking_agent\n", "note_taking_node = functools.partial(\n", " agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n", diff --git a/docs/docs/tutorials/plan-and-execute/plan-and-execute.ipynb b/docs/docs/tutorials/plan-and-execute/plan-and-execute.ipynb index 9279e8961..df794d961 100644 --- a/docs/docs/tutorials/plan-and-execute/plan-and-execute.ipynb +++ b/docs/docs/tutorials/plan-and-execute/plan-and-execute.ipynb @@ -393,6 +393,7 @@ "from typing import Literal\n", "from langgraph.graph import END\n", "\n", + "\n", "async def execute_step(state: PlanExecute):\n", " plan = state[\"plan\"]\n", " plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n", @@ -459,7 +460,7 @@ " \"replan\",\n", " # Next, we pass in the function that will determine which node is called next.\n", " should_end,\n", - " [\"agent\",END]\n", + " [\"agent\", END],\n", ")\n", "\n", "# Finally, we compile it!\n", diff --git a/docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb b/docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb index 894897982..e4ab0ffc8 100644 --- a/docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb +++ b/docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb @@ -185,7 +185,6 @@ "from pydantic import BaseModel, Field\n", "\n", "\n", - "\n", "# Data model\n", "class RouteQuery(BaseModel):\n", " \"\"\"Route a user query to the most relevant datasource.\"\"\"\n", diff --git a/docs/docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb b/docs/docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb index 1768637fc..ff2dad931 100644 --- a/docs/docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb +++ b/docs/docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb @@ -59,9 +59,10 @@ "source": [ "### LLM\n", "from langchain_ollama import ChatOllama\n", - "local_llm = 'llama3.2:3b-instruct-fp16'\n", + "\n", + "local_llm = \"llama3.2:3b-instruct-fp16\"\n", "llm = ChatOllama(model=local_llm, temperature=0)\n", - "llm_json_mode = ChatOllama(model=local_llm, temperature=0, format='json')" + "llm_json_mode = ChatOllama(model=local_llm, temperature=0, format=\"json\")" ] }, { @@ -76,19 +77,22 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "8a8792f5", "metadata": {}, "outputs": [], "source": [ - "import os, getpass\n", + "import os\n", + "import getpass\n", + "\n", "\n", "def _set_env(var: str):\n", " if not os.environ.get(var):\n", " os.environ[var] = getpass.getpass(f\"{var}: \")\n", "\n", + "\n", "_set_env(\"TAVILY_API_KEY\")\n", - "os.environ['TOKENIZERS_PARALLELISM'] = 'true'" + "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\"" ] }, { @@ -194,7 +198,7 @@ "import json\n", "from langchain_core.messages import HumanMessage, SystemMessage\n", "\n", - "# Prompt \n", + "# Prompt\n", "router_instructions = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n", "\n", "The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n", @@ -204,10 +208,27 @@ "Return JSON with single key, datasource, that is 'websearch' or 'vectorstore' depending on the question.\"\"\"\n", "\n", "# Test router\n", - "test_web_search = llm_json_mode.invoke([SystemMessage(content=router_instructions)] + [HumanMessage(content=\"Who is favored to win the NFC Championship game in the 2024 season?\")])\n", - "test_web_search_2 = llm_json_mode.invoke([SystemMessage(content=router_instructions)] + [HumanMessage(content=\"What are the models released today for llama3.2?\")])\n", - "test_vector_store = llm_json_mode.invoke([SystemMessage(content=router_instructions)] + [HumanMessage(content=\"What are the types of agent memory?\")])\n", - "print(json.loads(test_web_search.content), json.loads(test_web_search_2.content), json.loads(test_vector_store.content))" + "test_web_search = llm_json_mode.invoke(\n", + " [SystemMessage(content=router_instructions)]\n", + " + [\n", + " HumanMessage(\n", + " content=\"Who is favored to win the NFC Championship game in the 2024 season?\"\n", + " )\n", + " ]\n", + ")\n", + "test_web_search_2 = llm_json_mode.invoke(\n", + " [SystemMessage(content=router_instructions)]\n", + " + [HumanMessage(content=\"What are the models released today for llama3.2?\")]\n", + ")\n", + "test_vector_store = llm_json_mode.invoke(\n", + " [SystemMessage(content=router_instructions)]\n", + " + [HumanMessage(content=\"What are the types of agent memory?\")]\n", + ")\n", + "print(\n", + " json.loads(test_web_search.content),\n", + " json.loads(test_web_search_2.content),\n", + " json.loads(test_vector_store.content),\n", + ")" ] }, { @@ -228,9 +249,9 @@ } ], "source": [ - "### Retrieval Grader \n", + "### Retrieval Grader\n", "\n", - "# Doc grader instructions \n", + "# Doc grader instructions\n", "doc_grader_instructions = \"\"\"You are a grader assessing relevance of a retrieved document to a user question.\n", "\n", "If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant.\"\"\"\n", @@ -246,8 +267,13 @@ "question = \"What is Chain of thought prompting?\"\n", "docs = retriever.invoke(question)\n", "doc_txt = docs[1].page_content\n", - "doc_grader_prompt_formatted = doc_grader_prompt.format(document=doc_txt, question=question)\n", - "result = llm_json_mode.invoke([SystemMessage(content=doc_grader_instructions)] + [HumanMessage(content=doc_grader_prompt_formatted)])\n", + "doc_grader_prompt_formatted = doc_grader_prompt.format(\n", + " document=doc_txt, question=question\n", + ")\n", + "result = llm_json_mode.invoke(\n", + " [SystemMessage(content=doc_grader_instructions)]\n", + " + [HumanMessage(content=doc_grader_prompt_formatted)]\n", + ")\n", "json.loads(result.content)" ] }, @@ -287,10 +313,12 @@ "\n", "Answer:\"\"\"\n", "\n", + "\n", "# Post-processing\n", "def format_docs(docs):\n", " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", "\n", + "\n", "# Test\n", "docs = retriever.invoke(question)\n", "docs_txt = format_docs(docs)\n", @@ -318,9 +346,9 @@ } ], "source": [ - "### Hallucination Grader \n", + "### Hallucination Grader\n", "\n", - "# Hallucination grader instructions \n", + "# Hallucination grader instructions\n", "hallucination_grader_instructions = \"\"\"\n", "\n", "You are a teacher grading a quiz. \n", @@ -348,9 +376,14 @@ "\n", "Return JSON with two two keys, binary_score is 'yes' or 'no' score to indicate whether the STUDENT ANSWER is grounded in the FACTS. And a key, explanation, that contains an explanation of the score.\"\"\"\n", "\n", - "# Test using documents and generation from above \n", - "hallucination_grader_prompt_formatted = hallucination_grader_prompt.format(documents=docs_txt, generation=generation.content)\n", - "result = llm_json_mode.invoke([SystemMessage(content=hallucination_grader_instructions)] + [HumanMessage(content=hallucination_grader_prompt_formatted)])\n", + "# Test using documents and generation from above\n", + "hallucination_grader_prompt_formatted = hallucination_grader_prompt.format(\n", + " documents=docs_txt, generation=generation.content\n", + ")\n", + "result = llm_json_mode.invoke(\n", + " [SystemMessage(content=hallucination_grader_instructions)]\n", + " + [HumanMessage(content=hallucination_grader_prompt_formatted)]\n", + ")\n", "json.loads(result.content)" ] }, @@ -373,9 +406,9 @@ } ], "source": [ - "### Answer Grader \n", + "### Answer Grader\n", "\n", - "# Answer grader instructions \n", + "# Answer grader instructions\n", "answer_grader_instructions = \"\"\"You are a teacher grading a quiz. \n", "\n", "You will be given a QUESTION and a STUDENT ANSWER. \n", @@ -401,13 +434,18 @@ "\n", "Return JSON with two two keys, binary_score is 'yes' or 'no' score to indicate whether the STUDENT ANSWER meets the criteria. And a key, explanation, that contains an explanation of the score.\"\"\"\n", "\n", - "# Test \n", + "# Test\n", "question = \"What are the vision models released today as part of Llama 3.2?\"\n", "answer = \"The Llama 3.2 models released today include two vision models: Llama 3.2 11B Vision Instruct and Llama 3.2 90B Vision Instruct, which are available on Azure AI Model Catalog via managed compute. These models are part of Meta's first foray into multimodal AI and rival closed models like Anthropic's Claude 3 Haiku and OpenAI's GPT-4o mini in visual reasoning. They replace the older text-only Llama 3.1 models.\"\n", "\n", - "# Test using question and generation from above \n", - "answer_grader_prompt_formatted = answer_grader_prompt.format(question=question, generation=answer)\n", - "result = llm_json_mode.invoke([SystemMessage(content=answer_grader_instructions)] + [HumanMessage(content=answer_grader_prompt_formatted)])\n", + "# Test using question and generation from above\n", + "answer_grader_prompt_formatted = answer_grader_prompt.format(\n", + " question=question, generation=answer\n", + ")\n", + "result = llm_json_mode.invoke(\n", + " [SystemMessage(content=answer_grader_instructions)]\n", + " + [HumanMessage(content=answer_grader_prompt_formatted)]\n", + ")\n", "json.loads(result.content)" ] }, @@ -428,6 +466,7 @@ "source": [ "### Search\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", "web_search_tool = TavilySearchResults(k=3)" ] }, @@ -461,17 +500,19 @@ "from typing_extensions import TypedDict\n", "from typing import List, Annotated\n", "\n", + "\n", "class GraphState(TypedDict):\n", " \"\"\"\n", " Graph state is a dictionary that contains information we want to propagate to, and modify in, each graph node.\n", " \"\"\"\n", - " question : str # User question\n", - " generation : str # LLM generation\n", - " web_search : str # Binary decision to run web search\n", - " max_retries : int # Max number of retries for answer generation \n", - " answers : int # Number of answers generated\n", - " loop_step: Annotated[int, operator.add] \n", - " documents : List[str] # List of retrieved documents" + "\n", + " question: str # User question\n", + " generation: str # LLM generation\n", + " web_search: str # Binary decision to run web search\n", + " max_retries: int # Max number of retries for answer generation\n", + " answers: int # Number of answers generated\n", + " loop_step: Annotated[int, operator.add]\n", + " documents: List[str] # List of retrieved documents" ] }, { @@ -504,6 +545,7 @@ "from langchain.schema import Document\n", "from langgraph.graph import END\n", "\n", + "\n", "### Nodes\n", "def retrieve(state):\n", " \"\"\"\n", @@ -522,6 +564,7 @@ " documents = retriever.invoke(question)\n", " return {\"documents\": documents}\n", "\n", + "\n", "def generate(state):\n", " \"\"\"\n", " Generate answer using RAG on retrieved documents\n", @@ -536,12 +579,13 @@ " question = state[\"question\"]\n", " documents = state[\"documents\"]\n", " loop_step = state.get(\"loop_step\", 0)\n", - " \n", + "\n", " # RAG generation\n", " docs_txt = format_docs(documents)\n", " rag_prompt_formatted = rag_prompt.format(context=docs_txt, question=question)\n", " generation = llm.invoke([HumanMessage(content=rag_prompt_formatted)])\n", - " return {\"generation\": generation, \"loop_step\": loop_step+1}\n", + " return {\"generation\": generation, \"loop_step\": loop_step + 1}\n", + "\n", "\n", "def grade_documents(state):\n", " \"\"\"\n", @@ -558,14 +602,19 @@ " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", " question = state[\"question\"]\n", " documents = state[\"documents\"]\n", - " \n", + "\n", " # Score each doc\n", " filtered_docs = []\n", - " web_search = \"No\" \n", + " web_search = \"No\"\n", " for d in documents:\n", - " doc_grader_prompt_formatted = doc_grader_prompt.format(document=d.page_content, question=question)\n", - " result = llm_json_mode.invoke([SystemMessage(content=doc_grader_instructions)] + [HumanMessage(content=doc_grader_prompt_formatted)])\n", - " grade = json.loads(result.content)['binary_score']\n", + " doc_grader_prompt_formatted = doc_grader_prompt.format(\n", + " document=d.page_content, question=question\n", + " )\n", + " result = llm_json_mode.invoke(\n", + " [SystemMessage(content=doc_grader_instructions)]\n", + " + [HumanMessage(content=doc_grader_prompt_formatted)]\n", + " )\n", + " grade = json.loads(result.content)[\"binary_score\"]\n", " # Document relevant\n", " if grade.lower() == \"yes\":\n", " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", @@ -578,7 +627,8 @@ " web_search = \"Yes\"\n", " continue\n", " return {\"documents\": filtered_docs, \"web_search\": web_search}\n", - " \n", + "\n", + "\n", "def web_search(state):\n", " \"\"\"\n", " Web search based based on the question\n", @@ -601,11 +651,13 @@ " documents.append(web_results)\n", " return {\"documents\": documents}\n", "\n", + "\n", "### Edges\n", "\n", + "\n", "def route_question(state):\n", " \"\"\"\n", - " Route question to web search or RAG \n", + " Route question to web search or RAG\n", "\n", " Args:\n", " state (dict): The current graph state\n", @@ -615,15 +667,19 @@ " \"\"\"\n", "\n", " print(\"---ROUTE QUESTION---\")\n", - " route_question = llm_json_mode.invoke([SystemMessage(content=router_instructions)] + [HumanMessage(content=state[\"question\"])])\n", - " source = json.loads(route_question.content)['datasource']\n", - " if source == 'websearch':\n", + " route_question = llm_json_mode.invoke(\n", + " [SystemMessage(content=router_instructions)]\n", + " + [HumanMessage(content=state[\"question\"])]\n", + " )\n", + " source = json.loads(route_question.content)[\"datasource\"]\n", + " if source == \"websearch\":\n", " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n", " return \"websearch\"\n", - " elif source == 'vectorstore':\n", + " elif source == \"vectorstore\":\n", " print(\"---ROUTE QUESTION TO RAG---\")\n", " return \"vectorstore\"\n", "\n", + "\n", "def decide_to_generate(state):\n", " \"\"\"\n", " Determines whether to generate an answer, or add web search\n", @@ -643,13 +699,16 @@ " if web_search == \"Yes\":\n", " # All documents have been filtered check_relevance\n", " # We will re-generate a new query\n", - " print(\"---DECISION: NOT ALL DOCUMENTS ARE RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\")\n", + " print(\n", + " \"---DECISION: NOT ALL DOCUMENTS ARE RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\"\n", + " )\n", " return \"websearch\"\n", " else:\n", " # We have relevant documents, so generate answer\n", " print(\"---DECISION: GENERATE---\")\n", " return \"generate\"\n", "\n", + "\n", "def grade_generation_v_documents_and_question(state):\n", " \"\"\"\n", " Determines whether the generation is grounded in the document and answers question\n", @@ -665,21 +724,31 @@ " question = state[\"question\"]\n", " documents = state[\"documents\"]\n", " generation = state[\"generation\"]\n", - " max_retries = state.get(\"max_retries\", 3) # Default to 3 if not provided\n", + " max_retries = state.get(\"max_retries\", 3) # Default to 3 if not provided\n", "\n", - " hallucination_grader_prompt_formatted = hallucination_grader_prompt.format(documents=format_docs(documents), generation=generation.content)\n", - " result = llm_json_mode.invoke([SystemMessage(content=hallucination_grader_instructions)] + [HumanMessage(content=hallucination_grader_prompt_formatted)])\n", - " grade = json.loads(result.content)['binary_score']\n", + " hallucination_grader_prompt_formatted = hallucination_grader_prompt.format(\n", + " documents=format_docs(documents), generation=generation.content\n", + " )\n", + " result = llm_json_mode.invoke(\n", + " [SystemMessage(content=hallucination_grader_instructions)]\n", + " + [HumanMessage(content=hallucination_grader_prompt_formatted)]\n", + " )\n", + " grade = json.loads(result.content)[\"binary_score\"]\n", "\n", " # Check hallucination\n", " if grade == \"yes\":\n", " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", " # Check question-answering\n", " print(\"---GRADE GENERATION vs QUESTION---\")\n", - " # Test using question and generation from above \n", - " answer_grader_prompt_formatted = answer_grader_prompt.format(question=question, generation=generation.content)\n", - " result = llm_json_mode.invoke([SystemMessage(content=answer_grader_instructions)] + [HumanMessage(content=answer_grader_prompt_formatted)])\n", - " grade = json.loads(result.content)['binary_score']\n", + " # Test using question and generation from above\n", + " answer_grader_prompt_formatted = answer_grader_prompt.format(\n", + " question=question, generation=generation.content\n", + " )\n", + " result = llm_json_mode.invoke(\n", + " [SystemMessage(content=answer_grader_instructions)]\n", + " + [HumanMessage(content=answer_grader_prompt_formatted)]\n", + " )\n", + " grade = json.loads(result.content)[\"binary_score\"]\n", " if grade == \"yes\":\n", " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", " return \"useful\"\n", @@ -688,7 +757,7 @@ " return \"not useful\"\n", " else:\n", " print(\"---DECISION: MAX RETRIES REACHED---\")\n", - " return \"max retries\" \n", + " return \"max retries\"\n", " elif state[\"loop_step\"] <= max_retries:\n", " print(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", " return \"not supported\"\n", @@ -729,10 +798,10 @@ "workflow = StateGraph(GraphState)\n", "\n", "# Define the nodes\n", - "workflow.add_node(\"websearch\", web_search) # web search\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generate\n", + "workflow.add_node(\"websearch\", web_search) # web search\n", + "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", + "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", + "workflow.add_node(\"generate\", generate) # generate\n", "\n", "# Build graph\n", "workflow.set_conditional_entry_point(\n", @@ -798,7 +867,10 @@ "outputs": [], "source": [ "# Test on current events\n", - "inputs = {\"question\": \"What are the models released today for llama3.2?\", \"max_retries\": 3}\n", + "inputs = {\n", + " \"question\": \"What are the models released today for llama3.2?\",\n", + " \"max_retries\": 3,\n", + "}\n", "for event in graph.stream(inputs, stream_mode=\"values\"):\n", " print(event)" ] @@ -836,7 +908,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/docs/docs/tutorials/rag/langgraph_crag_local.ipynb b/docs/docs/tutorials/rag/langgraph_crag_local.ipynb index e93ad7337..639b861fa 100644 --- a/docs/docs/tutorials/rag/langgraph_crag_local.ipynb +++ b/docs/docs/tutorials/rag/langgraph_crag_local.ipynb @@ -699,7 +699,9 @@ " \"\"\"\n", " Find all tool calls in the messages returned\n", " \"\"\"\n", - " tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n", + " tool_calls = [\n", + " tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n", + " ]\n", " return tool_calls\n", "\n", "\n", diff --git a/docs/docs/tutorials/reflection/reflection.ipynb b/docs/docs/tutorials/reflection/reflection.ipynb index 0fc53b34d..e3c8b1274 100644 --- a/docs/docs/tutorials/reflection/reflection.ipynb +++ b/docs/docs/tutorials/reflection/reflection.ipynb @@ -58,6 +58,7 @@ " return\n", " os.environ[var] = getpass.getpass(var)\n", "\n", + "\n", "_set_if_undefined(\"TAVILY_API_KEY\")\n", "_set_if_undefined(\"FIREWORKS_API_KEY\")" ] @@ -108,8 +109,7 @@ " ]\n", ")\n", "llm = ChatFireworks(\n", - " model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n", - " max_tokens=32768\n", + " model=\"accounts/fireworks/models/mixtral-8x7b-instruct\", max_tokens=32768\n", ")\n", "generate = prompt | llm" ] @@ -331,15 +331,15 @@ "\n", "\n", "async def generation_node(state: State) -> State:\n", - " return {\"messages\": [await generate.ainvoke(state['messages'])]}\n", + " return {\"messages\": [await generate.ainvoke(state[\"messages\"])]}\n", "\n", "\n", "async def reflection_node(state: State) -> State:\n", " # Other messages we need to adjust\n", " cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n", " # First message is the original user request. We hold it the same for all nodes\n", - " translated = [state['messages'][0]] + [\n", - " cls_map[msg.type](content=msg.content) for msg in state['messages'][1:]\n", + " translated = [state[\"messages\"][0]] + [\n", + " cls_map[msg.type](content=msg.content) for msg in state[\"messages\"][1:]\n", " ]\n", " res = await reflect.ainvoke(translated)\n", " # We treat the output of this as human feedback for the generator\n", @@ -359,7 +359,6 @@ " return \"reflect\"\n", "\n", "\n", - "\n", "builder.add_conditional_edges(\"generate\", should_continue)\n", "builder.add_edge(\"reflect\", \"generate\")\n", "memory = MemorySaver()\n", @@ -406,13 +405,16 @@ } ], "source": [ - "async for event in graph.astream({\n", - " \"messages\": [\n", - " HumanMessage(\n", - " content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n", - " )\n", - " ],\n", - "}, config):\n", + "async for event in graph.astream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n", + " )\n", + " ],\n", + " },\n", + " config,\n", + "):\n", " print(event)\n", " print(\"---\")" ] diff --git a/docs/docs/tutorials/reflexion/reflexion.ipynb b/docs/docs/tutorials/reflexion/reflexion.ipynb index 73a4eee6f..102f48175 100644 --- a/docs/docs/tutorials/reflexion/reflexion.ipynb +++ b/docs/docs/tutorials/reflexion/reflexion.ipynb @@ -66,6 +66,7 @@ " return\n", " os.environ[var] = getpass.getpass(var)\n", "\n", + "\n", "_set_if_undefined(\"ANTHROPIC_API_KEY\")\n", "_set_if_undefined(\"TAVILY_API_KEY\")" ] @@ -192,7 +193,7 @@ " response = []\n", " for attempt in range(3):\n", " response = self.runnable.invoke(\n", - " {\"messages\": state['messages']}, {\"tags\": [f\"attempt:{attempt}\"]}\n", + " {\"messages\": state[\"messages\"]}, {\"tags\": [f\"attempt:{attempt}\"]}\n", " )\n", " try:\n", " self.validator.invoke(response)\n", @@ -259,7 +260,9 @@ "outputs": [], "source": [ "example_question = \"Why is reflection useful in AI?\"\n", - "initial = first_responder.respond({\"messages\":[HumanMessage(content=example_question)]})" + "initial = first_responder.respond(\n", + " {\"messages\": [HumanMessage(content=example_question)]}\n", + ")" ] }, { @@ -332,20 +335,26 @@ "import json\n", "\n", "revised = revisor.respond(\n", - " {\"messages\": [\n", - " HumanMessage(content=example_question),\n", - " initial['messages'],\n", - " ToolMessage(\n", - " tool_call_id=initial['messages'].tool_calls[0][\"id\"],\n", - " content=json.dumps(\n", - " tavily_tool.invoke(\n", - " {\"query\": initial['messages'].tool_calls[0][\"args\"][\"search_queries\"][0]}\n", - " )\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(content=example_question),\n", + " initial[\"messages\"],\n", + " ToolMessage(\n", + " tool_call_id=initial[\"messages\"].tool_calls[0][\"id\"],\n", + " content=json.dumps(\n", + " tavily_tool.invoke(\n", + " {\n", + " \"query\": initial[\"messages\"].tool_calls[0][\"args\"][\n", + " \"search_queries\"\n", + " ][0]\n", + " }\n", + " )\n", + " ),\n", " ),\n", - " ),\n", - " ]}\n", + " ]\n", + " }\n", ")\n", - "revised['messages']" + "revised[\"messages\"]" ] }, { @@ -439,7 +448,7 @@ "\n", "def event_loop(state: list):\n", " # in our case, we'll just stop after N plans\n", - " num_iterations = _get_num_iterations(state['messages'])\n", + " num_iterations = _get_num_iterations(state[\"messages\"])\n", " if num_iterations > MAX_ITERATIONS:\n", " return END\n", " return \"execute_tools\"\n", @@ -598,7 +607,7 @@ ")\n", "for i, step in enumerate(events):\n", " print(f\"Step {i}\")\n", - " step['messages'][-1].pretty_print()" + " step[\"messages\"][-1].pretty_print()" ] }, { diff --git a/docs/docs/tutorials/rewoo/rewoo.ipynb b/docs/docs/tutorials/rewoo/rewoo.ipynb index 6836eb6dd..324d7e5bd 100644 --- a/docs/docs/tutorials/rewoo/rewoo.ipynb +++ b/docs/docs/tutorials/rewoo/rewoo.ipynb @@ -317,7 +317,7 @@ " \"\"\"Worker node that executes the tools of a given plan.\"\"\"\n", " _step = _get_current_task(state)\n", " _, step_name, tool, tool_input = state[\"steps\"][_step - 1]\n", - " _results = (state[\"results\"] or {}) if \"results\" in state else {}\n", + " _results = (state[\"results\"] or {}) if \"results\" in state else {}\n", " for k, v in _results.items():\n", " tool_input = tool_input.replace(k, v)\n", " if tool == \"Google\":\n", @@ -363,7 +363,7 @@ "def solve(state: ReWOO):\n", " plan = \"\"\n", " for _plan, step_name, tool, tool_input in state[\"steps\"]:\n", - " _results = (state[\"results\"] or {}) if \"results\" in state else {}\n", + " _results = (state[\"results\"] or {}) if \"results\" in state else {}\n", " for k, v in _results.items():\n", " tool_input = tool_input.replace(k, v)\n", " step_name = step_name.replace(k, v)\n", @@ -464,7 +464,7 @@ ], "source": [ "# Print out the final result\n", - "print(s['solve']['result'])" + "print(s[\"solve\"][\"result\"])" ] }, { diff --git a/docs/docs/tutorials/storm/storm.ipynb b/docs/docs/tutorials/storm/storm.ipynb index 1ad2206a8..539affd66 100644 --- a/docs/docs/tutorials/storm/storm.ipynb +++ b/docs/docs/tutorials/storm/storm.ipynb @@ -847,7 +847,9 @@ "builder.add_edge(\"ask_question\", \"answer_question\")\n", "\n", "builder.add_edge(START, \"ask_question\")\n", - "interview_graph = builder.compile(checkpointer=False).with_config(run_name=\"Conduct Interviews\")" + "interview_graph = builder.compile(checkpointer=False).with_config(\n", + " run_name=\"Conduct Interviews\"\n", + ")" ] }, { diff --git a/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb b/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb index 6dbd9ab42..c68fb81e1 100644 --- a/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb +++ b/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb @@ -172,9 +172,7 @@ "\n", "\n", "summary_llm_chain = (\n", - " summary_prompt\n", - " | ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - " | StrOutputParser()\n", + " summary_prompt | ChatAnthropic(model=\"claude-3-haiku-20240307\") | StrOutputParser()\n", " # Customize the tracing name for easier organization\n", ").with_config(run_name=\"GenerateSummary\")\n", "summary_chain = summary_llm_chain | parse_summary\n",