diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 97ac204a5..ddf48fb0b 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -27,6 +27,8 @@ _MANUAL = { "streaming-events-from-within-tools-without-langchain.ipynb", "streaming-from-final-node.ipynb", "persistence.ipynb", + "input_output_schema.ipynb", + "pass_private_state.ipynb", "memory/manage-conversation-history.ipynb", "memory/delete-messages.ipynb", "memory/add-summary-conversation-history.ipynb", diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index 177b70287..8f0763f78 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -46,6 +46,9 @@ The first thing you do when you define a graph is define the `State` of the grap The main documented way to specify the schema of a graph is by using `TypedDict`. However, we also support [using a Pydantic BaseModel](../how-tos/state-model.ipynb) as your graph state to add **default values** and additional data validation. +By default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. See the [notebook here](../how-tos/input_output_schema.ipynb) for how to use. + +By default, all nodes in the graph will share the same state. This means that they will read and write to the same state channels. It is possible to have nodes write to private state channels inside the graph for internal node communication - see [this notebook](../how-tos/pass_private_state.ipynb) for how to do that. ### Reducers Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. Let's take a look at a few examples to understand them better. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index a344752e2..083661975 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -157,12 +157,15 @@ nav: - Handle tool calling errors: how-tos/tool-calling-errors.ipynb - 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 + - State Management: + - Use Pydantic model as state: how-tos/state-model.ipynb + - Use a context object in state: how-tos/state-context-key.ipynb + - Have a separate input and output schema: how-tos/input_output_schema.ipynb + - Pass private state between nodes inside the graph: how-tos/pass_private_state.ipynb - Other: - Run graph asynchronously: how-tos/async.ipynb - Visualize your graph: how-tos/visualization.ipynb - Add runtime configuration: how-tos/configuration.ipynb - - Use Pydantic model as state: how-tos/state-model.ipynb - - Use a context object in state: how-tos/state-context-key.ipynb - Add node retries: how-tos/node-retries.ipynb - Prebuilt ReAct Agent: - Create a ReAct agent: how-tos/create-react-agent.ipynb diff --git a/examples/input_output_schema.ipynb b/examples/input_output_schema.ipynb new file mode 100644 index 000000000..59f902361 --- /dev/null +++ b/examples/input_output_schema.ipynb @@ -0,0 +1,93 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f262985e-e973-4a27-9c9e-dbb3a06a35b7", + "metadata": {}, + "source": [ + "# How to define input/output schema for your graph\n", + "\n", + "By default, `StateGraph` takes in a single schema and all nodes are expected to communicate with that schema. However, it is also possible to define explicit input and output schemas for a graph. This is helpful if you want to draw a distinction between input and output keys.\n", + "\n", + "In this notebook we'll walk through an example of this. At a high level, in order to do this you simply have to pass in `input=..., output=...` when defining the graph. Let's see an example below!" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "6ec0eb77-874e-443e-8c73-93125b515106", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'answer': 'bye'}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langgraph.graph import StateGraph, START, END\n", + "from typing import TypedDict\n", + "\n", + "class InputState(TypedDict):\n", + " question: str\n", + "\n", + "class OutputState(TypedDict):\n", + " answer: str\n", + "\n", + "def answer_node(state: InputState):\n", + " return {\"answer\": \"bye\"}\n", + "\n", + "check = SqliteSaver.from_conn_string(\":memory:\")\n", + "graph = StateGraph(input=InputState, output=OutputState)\n", + "graph.add_node(answer_node)\n", + "graph.add_edge(START, \"answer_node\")\n", + "graph.add_edge(\"answer_node\", END)\n", + "graph = graph.compile()\n", + "\n", + "graph.invoke({\"question\": \"hi\"})" + ] + }, + { + "cell_type": "markdown", + "id": "6a68836f-98e1-4684-a8a6-c1473c73460c", + "metadata": {}, + "source": [ + "Notice that the output of invoke only includes the output schema." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b952a554-f2a4-4be3-81ab-2e08f0f441c2", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/pass_private_state.ipynb b/examples/pass_private_state.ipynb new file mode 100644 index 000000000..ca177a180 --- /dev/null +++ b/examples/pass_private_state.ipynb @@ -0,0 +1,126 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "47ed5db3-bda5-49e1-bf75-23e08c9a3af0", + "metadata": {}, + "source": [ + "# How to pass private state\n", + "\n", + "Oftentimes, you may want nodes to be able to pass state to eachv other that should NOT be part of the main schema of the graph. This is often useful because there may be information that is not needed as input/output (and therefore doesn't really make sense to have in the main schema) but is ABSOLUTELY needed as part of the intermediate working logic.\n", + "\n", + "Let's take a look at an example below. In this example, we will create a RAG pipeline that:\n", + "1. Takes in a user question\n", + "2. Uses an LLM to generate a search query\n", + "3. Retrieves documents for that generated query\n", + "4. Generates a final answer based on those documents\n", + "\n", + "We will have a separate node for each step. We will only have the `question` and `answer` on the overall state. However, we will need separate states for the `search_query` and the `documents` - we will pass these as private state keys.\n", + "\n", + "Let's look at an example!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3114c3ad-0ade-47ba-9488-53d6f7671578", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'question': 'foo', 'answer': 'fo\\n\\nfo\\n\\nfoo'}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langgraph.graph import StateGraph, START, END\n", + "from typing import TypedDict\n", + "\n", + "\n", + "# The overall state of the graph\n", + "class OverallState(TypedDict):\n", + " question: str\n", + " answer: str\n", + "\n", + "\n", + "# This is what the node that generates the query will return\n", + "class QueryOutputState(TypedDict):\n", + " query: str\n", + "\n", + "\n", + "# This is what the node that retrieves the documents will return\n", + "class DocumentOutputState(TypedDict):\n", + " docs: list[str]\n", + "\n", + "\n", + "# This is what the node that generates the final answer will take in\n", + "class GenerateInputState(OverallState, DocumentOutputState):\n", + " pass\n", + "\n", + "\n", + "# Node to generate query\n", + "def generate_query(state: OverallState) -> QueryOutputState:\n", + " # Replace this with real logic\n", + " return {\"query\": state[\"question\"][:2]}\n", + "\n", + "\n", + "# Node to retrieve documents\n", + "def retrieve_documents(state: QueryOutputState) -> DocumentOutputState:\n", + " # Replace this with real logic\n", + " return {\"docs\": [state['query']] * 2}\n", + "\n", + "\n", + "# Node to generate answer\n", + "def generate(state: GenerateInputState) -> OverallState:\n", + " return {\"answer\": \"\\n\\n\".join(state['docs'] + [state['question']])}\n", + "\n", + "\n", + "graph = StateGraph(OverallState)\n", + "graph.add_node(generate_query)\n", + "graph.add_node(retrieve_documents)\n", + "graph.add_node(generate)\n", + "graph.add_edge(START, \"generate_query\")\n", + "graph.add_edge(\"generate_query\", \"retrieve_documents\")\n", + "graph.add_edge(\"retrieve_documents\", \"generate\")\n", + "graph.add_edge(\"generate\", END)\n", + "graph = graph.compile()\n", + "\n", + "graph.invoke({\"question\": \"foo\"})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ffc2d8c-717f-42c9-b0aa-15b178a5cc8b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}