langchain-ai / langgraph

Build resilient language agents as graphs.
https://langchain-ai.github.io/langgraph/
MIT License
6.84k stars 1.1k forks source link

When Using Multiple States, Changing the Input State of a Node can Affect the State Fields Received by the Routing Function #2504

Open ahmed33033 opened 4 days ago

ahmed33033 commented 4 days ago

Checked other resources

Example Code

from operator import add
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END

class PersonState(TypedDict):
    name: str
    money: Annotated[int, add]

class PurchasesState(TypedDict):
    purchases: Annotated[list[str], add]
    total_cost: Annotated[int, add]

class OverallState(PersonState, PurchasesState):
    pass

# node_1 operation. 
def pay_salary(state: PersonState) -> OverallState:
    print(f"Node 1: {state}")
    return {"money": 1000}

# node_2 operation. 
def subtract_expenses(state: PurchasesState) -> OverallState:
    print(f"Node 2: {state}")
    return {'money': -100, "purchases": ["new iphone"], 'total_cost': 100}

# node_3 operation. 
def add_assistance(state: OverallState) -> OverallState:
    print(f"Node 3: {state}")
    return {"money": 100}

# Routing Function. 
def check_if_poor(state: OverallState):
    print(f"Routing function: {state}")
    # if poor, add asssitance
    if (state['money'] < 100): return "node_3"
    else: return END

graph = StateGraph(OverallState, input = PersonState)
graph.add_edge(START, 'node_1')
graph.add_node('node_1', pay_salary)
graph.add_edge('node_1', 'node_2')

graph.add_node('node_2', subtract_expenses)
graph.add_conditional_edges('node_2', check_if_poor)

graph.add_node('node_3', add_assistance)
graph.add_edge("node_3", END)

workflow = graph.compile()
output_dict = workflow.invoke({"name": "Ahmed"})
print(f"Final output: {output_dict}")

Error Message and Stack Trace (if applicable)

(No exception, but inaccurate console output)

CONSOLE OUTPUT:
Node 1: {'name': 'Ahmed', 'money': 0}
Node 2: {'purchases': [], 'total_cost': 0}
Routing function: {'money': -100, 'purchases': ['new iphone'], 'total_cost': 100}
Node 3: {'name': 'Ahmed', 'money': 900, 'purchases': ['new iphone'], 'total_cost': 100}
Final output: {'name': 'Ahmed', 'money': 1000, 'purchases': ['new iphone'], 'total_cost': 100}

Description

(Derived from Discussion Post #2197 , please see discussion post for detailed description)

The Console Output line that is inaccurate:

Routing function: {'money': -100, 'purchases': ['new iphone'], 'total_cost': 100}

How does money have a value of -100? In the method pay_salary, the money attribute is set to 1000. Then, in the subtract_expenses method, I subtract 100 from it. So, the money attribute should have a value of 900, not -100.

The line that is causing the weird output (after debugging)

def subtract_expenses(state: PurchasesState) -> OverallState:

How is it causing the issue?

When I change the inputted state from PurchasesState to OverallState, it works as expected, i.e. the output printed in the routing function check_if_poor is:

Routing function: {'money': 900, 'purchases': ['new iphone'], 'total_cost': 100, 'name': 'Ahmed'}

What's my question?

Why does changing the type of the inputted state in the function subtract_expenses lead me to receive different values for the money attribute in the routing function check_if_poor?

Thanks in advance!

System Info

System Information

OS: Windows OS Version: 10.0.19045 Python Version: 3.12.6 (tags/v3.12.6:a4a2d2b, Sep 6 2024, 20:11:23) [MSC v.1940 64 bit (AMD64)]

Package Information

langchain_core: 0.3.19 langchain: 0.3.7 langchain_community: 0.3.7 langsmith: 0.1.144 langchain_openai: 0.2.9 langchain_text_splitters: 0.3.2 langgraph: 0.2.53

Optional packages not installed

langserve

Other Dependencies

aiohttp: 3.11.7 async-timeout: Installed. No version info available. dataclasses-json: 0.6.7 httpx: 0.27.2 httpx-sse: 0.4.0 jsonpatch: 1.33 langgraph-checkpoint: 2.0.5 langgraph-sdk: 0.1.36 numpy: 1.26.4 openai: 1.55.0 orjson: 3.10.11 packaging: 24.2 pydantic: 2.10.0 pydantic-settings: 2.6.1 PyYAML: 6.0.2 requests: 2.32.3 requests-toolbelt: 1.0.0 SQLAlchemy: 2.0.35 tenacity: 9.0.0 tiktoken: 0.8.0 typing-extensions: 4.12.2

vbarda commented 3 days ago

@ahmed33033 great question -- that's because the state type annotation (input schema) in the node function acts as a "filter" -- currently you use PurchasesState in subtract_expenses, so the node ignores the money value (since PurchasesState doesn't have that key). You need to change it to PersonState, as that is what you're outputting from the previous node (pay_salary). that's also why OverallState works too, as it also includes money key.

def subtract_expenses(state: PersonState) -> OverallState:
    ...

hope this helps!

ahmed33033 commented 3 days ago

@ahmed33033 great question -- that's because the state type annotation (input schema) in the node function acts as a "filter" -- currently you use PurchasesState in subtract_expenses, so the node ignores the money value (since PurchasesState doesn't have that key). You need to change it to PersonState, as that is what you're outputting from the previous node (pay_salary). that's also why OverallState works too, as it also includes money key.

def subtract_expenses(state: PersonState) -> OverallState:
    ...

hope this helps!

Thank you so much for the quick reply!

Sorry, I think I didn't highlight the issue clearly.

You're absolutely right about the input state acting as a filer. But, the issue is mainly centered around the routing function check_if_poor, and not subtract_expenses. The issue is that the input schema of subtract_expenses affects the state fields that the routing function check_if_poor receives. Essentially, the graph flow should be as follows:

1 - Node pay_salary updates money to 1000. 2 - Node subtract_expenses issues another update to money of -100. This means that after subtract_expenses executes, money should have a value of 900. 3 - Routing function check_if_poor checks the value of money. It's supposed to be 900. But, as you can see from the first console output, the routing function check_if_poor somehow receives a money value of -100. This is the inaccuracy.

The issue is somehow "fixed" by changing the input state of subtract_expenses. But, the input state of subtract_expenses should not affect the state values that the routing function check_if_poor later receives. This is because langgraph nodes can "write to any state channel in the graph state" (langgraph glossary).

hinthornw commented 3 days ago

Hm ya this does feel surprising. The -100 is overwriting the state wholesale rather than being passed to the reducer (1000 - 900), despite the reducer of add being set in both states (overall state via inheritence) - thanks for reporting, Ahmed! We'll investigate