diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 402ecfc01..e50b1eab5 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -31,6 +31,7 @@ _MANUAL = { "pass_private_state.ipynb", "memory/manage-conversation-history.ipynb", "subgraphs-manage-state.ipynb", + "subgraph-transform-state.ipynb", "memory/delete-messages.ipynb", "memory/add-summary-conversation-history.ipynb", "persistence_postgres.ipynb", diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index d57333b6d..c0a717c4d 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -67,6 +67,7 @@ These guides show how to use different streaming modes. - [How to create subgraphs](subgraph.ipynb) - [How to manage state in subgraphs](subgraphs-manage-state.ipynb) +- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb) ## State Management diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 2efeafb7a..8064194e8 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -163,6 +163,7 @@ nav: - Subgraphs: - Create subgraphs: how-tos/subgraph.ipynb - Manage state in subgraphs: how-tos/subgraphs-manage-state.ipynb + - Transform inputs and outputs of a subgraph: how-tos/subgraph-transform-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 diff --git a/examples/subgraph-transform-state.ipynb b/examples/subgraph-transform-state.ipynb new file mode 100644 index 000000000..94c4b9ccf --- /dev/null +++ b/examples/subgraph-transform-state.ipynb @@ -0,0 +1,277 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to transform inputs and outputs of a subgraph\n", + "\n", + "It's possible that your subgraph state is completely independent from the parent graph state, i.e. there are no overlapping channels (keys) between the two. For example, you might have a supervisor agent that needs to produce a report with a help of multiple ReAct agents. ReAct agent subgraphs might keep track of a list of messages whereas the supervisor only needs user input and final report in its state, and doesn't need to keep track of messages.\n", + "\n", + "In such cases you need to transform the inputs to the subgraph before calling it and then transform its outputs before returning. This guide shows how to do that." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define graph and subgraphs\n", + "\n", + "Let's define 3 graphs:\n", + "- a parent graph\n", + "- a child subgraph that will be called by the parent graph\n", + "- a grandchild subgraph that will be called by the child graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define grandchild" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict\n", + "from langgraph.graph.state import StateGraph, START, END\n", + "\n", + "\n", + "class GrandChildState(TypedDict):\n", + " my_grandchild_key: str\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", + "grandchild = StateGraph(GrandChildState)\n", + "grandchild.add_node(\"grandchild_1\", grandchild_1)\n", + "\n", + "grandchild.add_edge(START, \"grandchild_1\")\n", + "grandchild.add_edge(\"grandchild_1\", END)\n", + "\n", + "grandchild_graph = grandchild.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'my_grandchild_key': 'hi Bob, how are you'}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grandchild_graph.invoke({\"my_grandchild_key\": \"hi Bob\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define child" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class ChildState(TypedDict):\n", + " my_child_key: str\n", + "\n", + "\n", + "def call_grandchild_graph(state: ChildState) -> ChildState:\n", + " # NOTE: parent or grandchild keys won't be accessible here\n", + " # we're transforming the state from the child state channels (`my_child_key`)\n", + " # to the child state channels (`my_grandchild_key`)\n", + " grandchild_graph_input = {\"my_grandchild_key\": state[\"my_child_key\"]}\n", + " # we're transforming the state from the grandchild state channels (`my_grandchild_key`)\n", + " # back to the child state channels (`my_child_key`)\n", + " grandchild_graph_output = grandchild_graph.invoke(grandchild_graph_input)\n", + " return {\"my_child_key\": grandchild_graph_output[\"my_grandchild_key\"] + \" today?\"}\n", + "\n", + "\n", + "child = StateGraph(ChildState)\n", + "# NOTE: we're passing a function here instead of just compiled graph (`child_graph`)\n", + "child.add_node(\"child_1\", call_grandchild_graph)\n", + "child.add_edge(START, \"child_1\")\n", + "child.add_edge(\"child_1\", END)\n", + "child_graph = child.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'my_child_key': 'hi Bob, how are you today?'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "child_graph.invoke({\"my_child_key\": \"hi Bob\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
Note
\n", + "\n",
+ " We're wrapping the grandchild_graph invocation in a separate function (call_grandchild_graph) that transforms the input state before calling the grandchild graph and then transforms the output of grandchild graph back to child graph state. If you just pass grandchild_graph directly to .add_node without the transformations, LangGraph will raise an error as there are no shared state channels (keys) between child and grandchild states.\n",
+ "
child_graph`)\n",
+ "parent.add_node(\"child\", call_child_graph)\n",
+ "parent.add_node(\"parent_2\", parent_2)\n",
+ "\n",
+ "parent.add_edge(START, \"parent_1\")\n",
+ "parent.add_edge(\"parent_1\", \"child\")\n",
+ "parent.add_edge(\"child\", \"parent_2\")\n",
+ "parent.add_edge(\"parent_2\", END)\n",
+ "\n",
+ "parent_graph = parent.compile()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note
\n", + "\n",
+ " We're wrapping the child_graph invocation in a separate function (call_child_graph) that transforms the input state before calling the child graph and then transforms the output of the child graph back to parent graph state. If you just pass child_graph directly to .add_node without the transformations, LangGraph will raise an error as there are no shared state channels (keys) between parent and child states.\n",
+ "