From fdbb33ddad21a484f8c7fbc62e8a29d125dc6fe0 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 22 Jul 2024 16:26:31 -0700 Subject: [PATCH 1/3] add private state --- docs/_scripts/copy_notebooks.py | 2 + docs/mkdocs.yml | 7 +- examples/input_output_schema.ipynb | 93 +++++++++++++++++++++ examples/pass_private_state.ipynb | 126 +++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 examples/input_output_schema.ipynb create mode 100644 examples/pass_private_state.ipynb diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 5954e7ef5..1ef9d1cb8 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/mkdocs.yml b/docs/mkdocs.yml index daf74c894..5e4b04f5c 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 - Prebuilt ReAct Agent: - Create a ReAct agent: how-tos/create-react-agent.ipynb - Add memory to a ReAct agent: how-tos/create-react-agent-memory.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..c540e1033 --- /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 eachother 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 therefor 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 +} From 8d3da565c4f152e2fdd59ce623dc34bc2858d8bd Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 22 Jul 2024 16:30:34 -0700 Subject: [PATCH 2/3] cr --- docs/docs/concepts/low_level.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index e658761cc..7692ff2b6 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. From 57c40026f860f65320453ce1cc4384ebb52ef81c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 23 Jul 2024 13:46:56 -0700 Subject: [PATCH 3/3] Update pass_private_state.ipynb --- examples/pass_private_state.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pass_private_state.ipynb b/examples/pass_private_state.ipynb index c540e1033..ca177a180 100644 --- a/examples/pass_private_state.ipynb +++ b/examples/pass_private_state.ipynb @@ -7,7 +7,7 @@ "source": [ "# How to pass private state\n", "\n", - "Oftentimes, you may want nodes to be able to pass state to eachother 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 therefor doesn't really make sense to have in the main schema) but is ABSOLUTELY needed as part of the intermediate working logic.\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",