docs: subgraphs manage state (#1543)

This commit is contained in:
Harrison Chase
2024-09-04 17:57:06 -04:00
committed by GitHub
parent f66493f6db
commit 82c989feea
5 changed files with 1113 additions and 36 deletions
+1
View File
@@ -30,6 +30,7 @@ _MANUAL = {
"input_output_schema.ipynb",
"pass_private_state.ipynb",
"memory/manage-conversation-history.ipynb",
"subgraphs-manage-state.ipynb",
"memory/delete-messages.ipynb",
"memory/add-summary-conversation-history.ipynb",
"persistence_postgres.ipynb",
+5 -1
View File
@@ -12,7 +12,6 @@ Welcome to the LangGraph how-to guides! These guides provide practical, step-by-
LangGraph is known for being a highly controllable agent framework.
These how-to guides show how to achieve that controllability.
- [How to create subgraphs](subgraph.ipynb)
- [How to create branches for parallel execution](branching.ipynb)
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
- [How to control graph recursion limit](recursion-limit.ipynb)
@@ -64,6 +63,11 @@ These guides show how to use different streaming modes.
- [How to pass config to tools](pass-config-to-tools.ipynb)
- [How to handle large numbers of tools](many-tools.ipynb)
## Subgraphs
- [How to create subgraphs](subgraph.ipynb)
- [How to manage state in subgraphs](subgraphs-manage-state.ipynb)
## State Management
- [Use Pydantic model as state](state-model.ipynb)
+3 -1
View File
@@ -126,7 +126,6 @@ nav:
- "How-to Guides":
- "how-tos/index.md"
- Controllability:
- Create subgraphs: how-tos/subgraph.ipynb
- Create branches for parallel execution: how-tos/branching.ipynb
- Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb
- Control graph recursion limit: how-tos/recursion-limit.ipynb
@@ -161,6 +160,9 @@ nav:
- Pass graph state to tools: how-tos/pass-run-time-values-to-tools.ipynb
- Pass config to tools: how-tos/pass-config-to-tools.ipynb
- Handle many tools: how-tos/many-tools.ipynb
- Subgraphs:
- Create subgraphs: how-tos/subgraph.ipynb
- Manage state in subgraphs: how-tos/subgraphs-manage-state.ipynb
- State Management:
- Use Pydantic model as state: how-tos/state-model.ipynb
- Use a context object in state: how-tos/state-context-key.ipynb
File diff suppressed because one or more lines are too long
+236 -34
View File
@@ -46,7 +46,10 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"]
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_openai"
]
},
{
"cell_type": "markdown",
@@ -62,7 +65,18 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": ["import getpass\nimport os\n\n\ndef _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\")"]
"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",
@@ -78,7 +92,10 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
},
{
"cell_type": "markdown",
@@ -96,7 +113,22 @@
"id": "f5319e01",
"metadata": {},
"outputs": [],
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
"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\n",
"# (with more 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",
@@ -116,7 +148,19 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The weather is cloudy with a chance of meatballs.\"]\n\n\ntools = [search]"]
"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 weather is cloudy with a chance of meatballs.\"]\n",
"\n",
"\n",
"tools = [search]"
]
},
{
"cell_type": "markdown",
@@ -133,7 +177,11 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tool_node = ToolNode(tools)"
]
},
{
"cell_type": "markdown",
@@ -157,7 +205,11 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
},
{
"cell_type": "markdown",
@@ -175,7 +227,9 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": ["model = model.bind_tools(tools)"]
"source": [
"model = model.bind_tools(tools)"
]
},
{
"cell_type": "markdown",
@@ -210,7 +264,20 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"continue\", \"end\"]:\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 else:\n return \"continue\""]
"source": [
"from typing import Literal\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state: State) -> Literal[\"continue\", \"end\"]:\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",
" else:\n",
" return \"continue\""
]
},
{
"cell_type": "markdown",
@@ -228,7 +295,50 @@
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
"metadata": {},
"outputs": [],
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n\n# Define the two nodes we will cycle between\ndef call_model(state: State) -> State:\n return {\"messages\": model.invoke(state[\"messages\"])}\n\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.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.\nworkflow.add_edge(\"action\", \"agent\")"]
"source": [
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"\n",
"# Define the two nodes we will cycle between\n",
"def call_model(state: State) -> State:\n",
" return {\"messages\": model.invoke(state[\"messages\"])}\n",
"\n",
"\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\")"
]
},
{
"cell_type": "markdown",
@@ -246,7 +356,11 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
"source": ["from langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()"]
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = MemorySaver()"
]
},
{
"cell_type": "code",
@@ -254,7 +368,12 @@
"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\napp = workflow.compile(checkpointer=memory)"]
"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",
@@ -281,7 +400,15 @@
"output_type": "display_data"
}
],
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
"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",
@@ -312,7 +439,14 @@
]
}
],
"source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"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": "markdown",
@@ -350,7 +484,9 @@
"output_type": "execute_result"
}
],
"source": ["app.get_state(config).values"]
"source": [
"app.get_state(config).values"
]
},
{
"cell_type": "markdown",
@@ -379,7 +515,9 @@
"output_type": "execute_result"
}
],
"source": ["app.get_state(config).next"]
"source": [
"app.get_state(config).next"
]
},
{
"cell_type": "markdown",
@@ -426,7 +564,12 @@
]
}
],
"source": ["config = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"input_message = HumanMessage(content=\"what is the weather in sf currently\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
@@ -466,7 +609,9 @@
"id": "5a68afc0-606f-4294-a872-b2b563be0d69",
"metadata": {},
"outputs": [],
"source": ["app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
"source": [
"app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
},
{
"cell_type": "code",
@@ -490,7 +635,14 @@
]
}
],
"source": ["config = {\"configurable\": {\"thread_id\": \"4\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app_w_interrupt.stream(\n {\"messages\": [input_message]}, config, stream_mode=\"values\"\n):\n event[\"messages\"][-1].pretty_print()"]
"source": [
"config = {\"configurable\": {\"thread_id\": \"4\"}}\n",
"input_message = HumanMessage(content=\"what is the weather in sf currently\")\n",
"for event in app_w_interrupt.stream(\n",
" {\"messages\": [input_message]}, config, stream_mode=\"values\"\n",
"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
@@ -526,7 +678,10 @@
"output_type": "execute_result"
}
],
"source": ["current_values = app_w_interrupt.get_state(config)\ncurrent_values.next"]
"source": [
"current_values = app_w_interrupt.get_state(config)\n",
"current_values.next"
]
},
{
"cell_type": "markdown",
@@ -555,7 +710,9 @@
"output_type": "execute_result"
}
],
"source": ["current_values.values[\"messages\"][-1].tool_calls"]
"source": [
"current_values.values[\"messages\"][-1].tool_calls"
]
},
{
"cell_type": "markdown",
@@ -571,7 +728,11 @@
"id": "060e2e33-1f6a-40ef-850e-161b308986fb",
"metadata": {},
"outputs": [],
"source": ["current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n \"query\"\n] = \"weather in San Francisco today\""]
"source": [
"current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n",
" \"query\"\n",
"] = \"weather in San Francisco today\""
]
},
{
"cell_type": "code",
@@ -591,7 +752,9 @@
"output_type": "execute_result"
}
],
"source": ["app_w_interrupt.update_state(config, current_values.values)"]
"source": [
"app_w_interrupt.update_state(config, current_values.values)"
]
},
{
"cell_type": "markdown",
@@ -629,7 +792,9 @@
"output_type": "execute_result"
}
],
"source": ["app_w_interrupt.get_state(config).values"]
"source": [
"app_w_interrupt.get_state(config).values"
]
},
{
"cell_type": "code",
@@ -648,7 +813,9 @@
"output_type": "execute_result"
}
],
"source": ["app_w_interrupt.get_state(config).next"]
"source": [
"app_w_interrupt.get_state(config).next"
]
},
{
"cell_type": "markdown",
@@ -679,7 +846,11 @@
]
}
],
"source": ["for event in app_w_interrupt.stream(None, config):\n for v in event.values():\n print(v)"]
"source": [
"for event in app_w_interrupt.stream(None, config):\n",
" for v in event.values():\n",
" print(v)"
]
},
{
"cell_type": "markdown",
@@ -726,7 +897,13 @@
]
}
],
"source": ["for state in app_w_interrupt.get_state_history(config):\n print(state)\n print(\"--\")\n if len(state.values[\"messages\"]) == 2:\n to_replay = state"]
"source": [
"for state in app_w_interrupt.get_state_history(config):\n",
" print(state)\n",
" print(\"--\")\n",
" if len(state.values[\"messages\"]) == 2:\n",
" to_replay = state"
]
},
{
"cell_type": "markdown",
@@ -754,7 +931,9 @@
"output_type": "execute_result"
}
],
"source": ["to_replay.values"]
"source": [
"to_replay.values"
]
},
{
"cell_type": "code",
@@ -773,7 +952,9 @@
"output_type": "execute_result"
}
],
"source": ["to_replay.next"]
"source": [
"to_replay.next"
]
},
{
"cell_type": "markdown",
@@ -806,7 +987,11 @@
]
}
],
"source": ["for event in app_w_interrupt.stream(None, to_replay.config):\n for v in event.values():\n print(v)"]
"source": [
"for event in app_w_interrupt.stream(None, to_replay.config):\n",
" for v in event.values():\n",
" print(v)"
]
},
{
"cell_type": "markdown",
@@ -834,7 +1019,18 @@
"id": "b084f141-5800-487b-b115-d2e58421b963",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import AIMessage\n\nbranch_config = app_w_interrupt.update_state(\n to_replay.config,\n {\n \"messages\": [\n AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n ]\n },\n)"]
"source": [
"from langchain_core.messages import AIMessage\n",
"\n",
"branch_config = app_w_interrupt.update_state(\n",
" to_replay.config,\n",
" {\n",
" \"messages\": [\n",
" AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n",
" ]\n",
" },\n",
")"
]
},
{
"cell_type": "code",
@@ -842,7 +1038,9 @@
"id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641",
"metadata": {},
"outputs": [],
"source": ["branch_state = app_w_interrupt.get_state(branch_config)"]
"source": [
"branch_state = app_w_interrupt.get_state(branch_config)"
]
},
{
"cell_type": "code",
@@ -862,7 +1060,9 @@
"output_type": "execute_result"
}
],
"source": ["branch_state.values"]
"source": [
"branch_state.values"
]
},
{
"cell_type": "code",
@@ -881,7 +1081,9 @@
"output_type": "execute_result"
}
],
"source": ["branch_state.next"]
"source": [
"branch_state.next"
]
},
{
"cell_type": "markdown",