From 82905297fd365fda77bef12df7f8a38126a679ee Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:47:17 +0200 Subject: [PATCH] docs: Add Pydantic usage examples and runtime coercion documentation (#3588) ## Description This PR enhances the state-model documentation by adding comprehensive examples for advanced Pydantic usage in LangGraph. It addresses issue #2745 regarding the need for better documentation of Pydantic schema behavior. ### Changes - Added new section on Advanced Pydantic Model Usage - Added examples for serialization behavior with nested models - Added section on runtime type coercion with examples - Added documentation for proper message type handling (BaseMessage vs AnyMessage) - Updated Pydantic error URLs to latest version ### Related Issues Closes #2745 ### Testing - All notebook cells have been executed and outputs verified - Examples demonstrate proper usage patterns - Error cases are properly documented ### Documentation The changes are documentation-focused and include: - New examples for complex Pydantic models - Runtime coercion behavior examples - Message type handling best practices ### Reviewers @eyurtsev --- docs/docs/how-tos/state-model.ipynb | 220 ++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/docs/docs/how-tos/state-model.ipynb b/docs/docs/how-tos/state-model.ipynb index 221a4738d..4448170cf 100644 --- a/docs/docs/how-tos/state-model.ipynb +++ b/docs/docs/how-tos/state-model.ipynb @@ -266,6 +266,226 @@ " print(\"An exception was raised because bad_node sets `a` to an integer.\")\n", " print(e)" ] + }, + { + "cell_type": "markdown", + "id": "2270bc3c", + "metadata": {}, + "source": [ + "## Multiple Nodes\n", + "\n", + "Run-time validation will also work in a multi-node graph. In the example below `bad_node` updates `a` to an integer. \n", + "\n", + "Because run-time validation occurs on **inputs**, the validation error will occur when `ok_node` is called (not when `bad_node` returns an update to the state which is inconsistent with the schema)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d832cdcc", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, START, END\n", + "from typing_extensions import TypedDict\n", + "\n", + "from pydantic import BaseModel\n", + "\n", + "\n", + "# The overall state of the graph (this is the public state shared across nodes)\n", + "class OverallState(BaseModel):\n", + " a: str\n", + "\n", + "\n", + "def bad_node(state: OverallState):\n", + " return {\n", + " \"a\": 123 # Invalid\n", + " }\n", + "\n", + "\n", + "def ok_node(state: OverallState):\n", + " return {\"a\": \"goodbye\"}\n", + "\n", + "\n", + "# Build the state graph\n", + "builder = StateGraph(OverallState)\n", + "builder.add_node(bad_node)\n", + "builder.add_node(ok_node)\n", + "builder.add_edge(START, \"bad_node\")\n", + "builder.add_edge(\"bad_node\", \"ok_node\")\n", + "builder.add_edge(\"ok_node\", END)\n", + "graph = builder.compile()\n", + "\n", + "# Test the graph with a valid input\n", + "try:\n", + " graph.invoke({\"a\": \"hello\"})\n", + "except Exception as e:\n", + " print(\"An exception was raised because bad_node sets `a` to an integer.\")\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "456b1f77", + "metadata": {}, + "source": [ + "## Advanced Pydantic Model Usage\n", + "\n", + "This section covers more advanced topics when using Pydantic models with LangGraph.\n", + "\n", + "### Serialization Behavior\n", + "\n", + "When using Pydantic models as state schemas, it's important to understand how serialization works, especially when:\n", + "- Passing Pydantic objects as inputs\n", + "- Receiving outputs from the graph\n", + "- Working with nested Pydantic models\n", + "\n", + "Let's see these behaviors in action:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e919cdc", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, START, END\n", + "from pydantic import BaseModel\n", + "\n", + "class NestedModel(BaseModel):\n", + " value: str\n", + "\n", + "class ComplexState(BaseModel):\n", + " text: str\n", + " count: int\n", + " nested: NestedModel\n", + "\n", + "def process_node(state: ComplexState):\n", + " # Node receives a validated Pydantic object\n", + " print(f\"Input state type: {type(state)}\")\n", + " print(f\"Nested type: {type(state.nested)}\")\n", + " \n", + " # Return a dictionary update\n", + " return {\"text\": state.text + \" processed\", \"count\": state.count + 1}\n", + "\n", + "# Build the graph\n", + "builder = StateGraph(ComplexState)\n", + "builder.add_node(\"process\", process_node)\n", + "builder.add_edge(START, \"process\")\n", + "builder.add_edge(\"process\", END)\n", + "graph = builder.compile()\n", + "\n", + "# Create a Pydantic instance for input\n", + "input_state = ComplexState(text=\"hello\", count=0, nested=NestedModel(value=\"test\"))\n", + "print(f\"Input object type: {type(input_state)}\")\n", + "\n", + "# Invoke graph with a Pydantic instance\n", + "result = graph.invoke(input_state)\n", + "print(f\"Output type: {type(result)}\")\n", + "print(f\"Output content: {result}\")\n", + "\n", + "# Convert back to Pydantic model if needed\n", + "output_model = ComplexState(**result)\n", + "print(f\"Converted back to Pydantic: {type(output_model)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f13f28ce", + "metadata": {}, + "source": [ + "### Runtime Type Coercion\n", + "\n", + "Pydantic performs runtime type coercion for certain data types. This can be helpful but also lead to unexpected behavior if you're not aware of it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faf59316", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, START, END\n", + "from pydantic import BaseModel\n", + "\n", + "class CoercionExample(BaseModel):\n", + " # Pydantic will coerce string numbers to integers\n", + " number: int\n", + " # Pydantic will parse string booleans to bool\n", + " flag: bool\n", + "\n", + "def inspect_node(state: CoercionExample):\n", + " print(f\"number: {state.number} (type: {type(state.number)})\")\n", + " print(f\"flag: {state.flag} (type: {type(state.flag)})\")\n", + " return {}\n", + "\n", + "builder = StateGraph(CoercionExample)\n", + "builder.add_node(\"inspect\", inspect_node)\n", + "builder.add_edge(START, \"inspect\")\n", + "builder.add_edge(\"inspect\", END)\n", + "graph = builder.compile()\n", + "\n", + "# Demonstrate coercion with string inputs that will be converted\n", + "result = graph.invoke({\"number\": \"42\", \"flag\": \"true\"})\n", + "\n", + "# This would fail with a validation error\n", + "try:\n", + " graph.invoke({\"number\": \"not-a-number\", \"flag\": \"true\"})\n", + "except Exception as e:\n", + " print(f\"\\nExpected validation error: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2844475b", + "metadata": {}, + "source": [ + "### Working with Message Models\n", + "\n", + "When working with LangChain message types in your state schema, there are important considerations for serialization. You should use `AnyMessage` (rather than `BaseMessage`) for proper serialization/deserialization when using message objects over the wire:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd0734b0", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, START, END\n", + "from pydantic import BaseModel\n", + "from langchain_core.messages import HumanMessage, AIMessage, BaseMessage\n", + "from typing import List\n", + "\n", + "class ChatState(BaseModel):\n", + " messages: List[BaseMessage] \n", + " context: str\n", + "\n", + "def add_message(state: ChatState):\n", + " return {\"messages\": state.messages + [AIMessage(content=\"Hello there!\")]}\n", + "\n", + "builder = StateGraph(ChatState)\n", + "builder.add_node(\"add_message\", add_message)\n", + "builder.add_edge(START, \"add_message\")\n", + "builder.add_edge(\"add_message\", END)\n", + "graph = builder.compile()\n", + "\n", + "# Create input with a message\n", + "initial_state = ChatState(\n", + " messages=[HumanMessage(content=\"Hi\")],\n", + " context=\"Customer support chat\"\n", + ")\n", + "\n", + "result = graph.invoke(initial_state)\n", + "print(f\"Output: {result}\")\n", + "\n", + "# Convert back to Pydantic model to see message types\n", + "output_model = ChatState(**result)\n", + "for i, msg in enumerate(output_model.messages):\n", + " print(f\"Message {i}: {type(msg).__name__} - {msg.content}\")" + ] } ], "metadata": {