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": [ + "
\n", + "

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", + "

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that child and grandchild subgraphs have their own, **independent** state that is not shared with the parent graph." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define parent" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "class ParentState(TypedDict):\n", + " my_key: str\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", + "def parent_2(state: ParentState) -> ParentState:\n", + " return {\"my_key\": state[\"my_key\"] + \" bye!\"}\n", + "\n", + "\n", + "def call_child_graph(state: ParentState) -> ParentState:\n", + " # we're transforming the state from the parent state channels (`my_key`)\n", + " # to the child state channels (`my_child_key`)\n", + " child_graph_input = {\"my_child_key\": state[\"my_key\"]}\n", + " # we're transforming the state from the child state channels (`my_child_key`)\n", + " # back to the parent state channels (`my_key`)\n", + " child_graph_output = child_graph.invoke(child_graph_input)\n", + " return {\"my_key\": child_graph_output[\"my_child_key\"]}\n", + "\n", + "\n", + "parent = StateGraph(ParentState)\n", + "parent.add_node(\"parent_1\", parent_1)\n", + "# NOTE: we're passing a function here instead of just a compiled graph (`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": [ + "
\n", + "

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", + "

\n", + "
\n", + "\n", + "Let's run the parent graph and make sure it correctly calls both the child and grandchild subgraphs:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'my_key': 'hi Bob, how are you today? bye!'}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "parent_graph.invoke({\"my_key\": \"Bob\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perfect! The parent graph correctly calls both the child and grandchild subgraphs (which we know since the \", how are you\" and \"today?\" are added to our original \"my_key\" state value)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langgraph", + "language": "python", + "name": "langgraph" + }, + "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": 4 +}