Validation Errors When Passing Graph State To Tools #250

Closed
opened 2026-02-20 17:33:40 -05:00 by yindo · 2 comments
Owner

Originally created by @fletchertyler914 on GitHub (Sep 30, 2024).

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangGraph/LangChain documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph/LangChain rather than my code.
  • I am sure this is better as an issue rather than a GitHub discussion, since this is a LangGraph bug and not a design question.

Example Code

import json
from typing import Annotated, Sequence, TypedDict

from langchain_core.tools import tool
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import ToolMessage, SystemMessage, BaseMessage

from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import InjectedState
from langgraph.graph.message import add_messages


class AgentState(TypedDict):
    """The state of the agent."""
    messages: Annotated[Sequence[BaseMessage], add_messages]


# Define a dummy get_weather tool with no state argument
@tool(parse_docstring=True)
def get_weather(location: str):
    """
    Call to get the weather from a specific location.

    Args:
        location: The location to get the weather from.
    """
    # This is a placeholder for the actual implementation
    # Don't let the LLM know this though 😊
    if any([city in location.lower() for city in ['sf', 'san francisco']]):
        return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈."
    else:
        return f"I am not sure what the weather is in {location}"


# ONLY add the (injected) state argument to this tool to show its broken
@tool(parse_docstring=True)
def get_weather_with_state(
    location: str,
    state: Annotated[dict, InjectedState]
):
    """
    Call to get the BROKEN weather from a specific location. Use this if a user asks for BROKEN weather only, otherwise use get_weather.

    Args:
        location: The location to get the weather from.
    """
    # This is a placeholder for the actual implementation
    # Don't let the LLM know this though 😊
    print('get_weather state: ', state)

    if any([city in location.lower() for city in ['sf', 'san francisco']]):
        return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈."
    else:
        return f"I am not sure what the weather is in {location}"


# Define our tools
tools = [get_weather, get_weather_with_state]
tools_by_name = {tool.name: tool for tool in tools}

# Define our model
model = ChatOpenAI(model="gpt-4o-mini")
model = model.bind_tools(tools)


# Define our tool node
def tool_node(state: AgentState):
    outputs = []
    for tool_call in state['messages'][-1].tool_calls:
        tool_result = tools_by_name[tool_call["name"]].invoke(
            tool_call["args"]
        )
        outputs.append(
            ToolMessage(
                content=json.dumps(tool_result),
                name=tool_call["name"],
                tool_call_id=tool_call["id"],
            )
        )
    return {"messages": outputs}


# Define the node that calls the model
def call_model(
    state: AgentState,
    config: RunnableConfig,
):
    # this is similar to customizing the create_react_agent with state_modifier, but is a lot more flexible
    system_prompt = SystemMessage(
        "You are a helpful AI assistant, please respond to the users query to the best of your ability!")

    response = model.invoke([system_prompt] + state['messages'], config)
    # We return a list, because this will get added to the existing list
    return {"messages": [response]}


# Define the conditional edge that determines whether to continue or not
def should_continue(state: AgentState):
    messages = state["messages"]
    last_message = messages[-1]
    # If there is no function call, then we finish
    if not last_message.tool_calls:
        return "end"
    # Otherwise if there is, we continue
    else:
        return "continue"


# Define a new graph
workflow = StateGraph(AgentState)

# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)

# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")

# We now add a conditional edge
workflow.add_conditional_edges(
    # First, we define the start node. We use `agent`.
    # This means these are the edges taken after the `agent` node is called.
    "agent",
    # Next, we pass in the function that will determine which node is called next.
    should_continue,
    # Finally we pass in a mapping.
    # The keys are strings, and the values are other nodes.
    # END is a special node marking that the graph should finish.
    # What will happen is we will call `should_continue`, and then the output of that
    # will be matched against the keys in this mapping.
    # Based on which one it matches, that node will then be called.
    {
        # If `tools`, then we call the tool node.
        "continue": "tools",
        # Otherwise we finish.
        "end": END,
    },
)

# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("tools", "agent")

# Now we can compile and visualize our graph
graph = workflow.compile()


# Helper function for formatting the stream nicely
def print_stream(stream):
    for s in stream:
        message = s["messages"][-1]
        if isinstance(message, tuple):
            print(message)
        else:
            message.pretty_print()


# invoke the stateless (working) tool first to show it works
inputs = {"messages": [("user", "what is the weather in sf")]}
print_stream(graph.stream(inputs, stream_mode="values"))

# invoke the stateful (non-working) tool next to show its broken (pydantic validation errors)
inputs = {"messages": [("user", "what is the BROKEN weather in sf")]}
print_stream(graph.stream(inputs, stream_mode="values"))

Error Message and Stack Trace (if applicable)

---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[1], line 166
    164 # invoke the stateful (non-working) tool next to show its broken (pydantic validation errors)
    165 inputs = {"messages": [("user", "what is the BROKEN weather in sf")]}
--> 166 print_stream(graph.stream(inputs, stream_mode="values"))

Cell In[1], line 152
    151 def print_stream(stream):
--> 152     for s in stream:
    153         message = s["messages"][-1]
    154         if isinstance(message, tuple):

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/pregel/__init__.py:1298, in Pregel.stream(self, input, config, stream_mode, output_keys, interrupt_before, interrupt_after, debug, subgraphs)
   1287     # Similarly to Bulk Synchronous Parallel / Pregel model
   1288     # computation proceeds in steps, while there are channel updates
   1289     # channel updates from step N are only visible in step N+1
   1290     # channels are guaranteed to be immutable for the duration of the step,
   1291     # with channel updates applied only at the transition between steps
   1292     while loop.tick(
   1293         input_keys=self.input_channels,
   1294         interrupt_before=interrupt_before_,
   1295         interrupt_after=interrupt_after_,
   1296         manager=run_manager,
   1297     ):
-> 1298         for _ in runner.tick(
   1299             loop.tasks.values(),
   1300             timeout=self.step_timeout,
   1301             retry_policy=self.retry_policy,
   1302             get_waiter=get_waiter,
   1303         ):
   1304             # emit output
   1305             yield from output()
   1306 # emit output

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/pregel/runner.py:56, in PregelRunner.tick(self, tasks, reraise, timeout, retry_policy, get_waiter)
     54 t = tasks[0]
     55 try:
---> 56     run_with_retry(t, retry_policy)
     57     self.commit(t, None)
     58 except Exception as exc:

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/pregel/retry.py:29, in run_with_retry(task, retry_policy)
     27 task.writes.clear()
     28 # run the task
---> 29 task.proc.invoke(task.input, config)
     30 # if successful, end
     31 break

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/utils/runnable.py:405, in RunnableSeq.invoke(self, input, config, **kwargs)
    403 context.run(_set_config_context, config)
    404 if i == 0:
--> 405     input = context.run(step.invoke, input, config, **kwargs)
    406 else:
    407     input = context.run(step.invoke, input, config)

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/utils/runnable.py:181, in RunnableCallable.invoke(self, input, config, **kwargs)
    179 else:
    180     context.run(_set_config_context, config)
--> 181     ret = context.run(self.func, input, **kwargs)
    182 if isinstance(ret, Runnable) and self.recurse:
    183     return ret.invoke(input, config)

Cell In[1], line 71
     69 outputs = []
     70 for tool_call in state['messages'][-1].tool_calls:
---> 71     tool_result = tools_by_name[tool_call["name"]].invoke(
     72         tool_call["args"]
     73     )
     74     outputs.append(
     75         ToolMessage(
     76             content=json.dumps(tool_result),
   (...)
     79         )
     80     )
     81 return {"messages": outputs}

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:485, in BaseTool.invoke(self, input, config, **kwargs)
    478 def invoke(
    479     self,
    480     input: Union[str, dict, ToolCall],
    481     config: Optional[RunnableConfig] = None,
    482     **kwargs: Any,
    483 ) -> Any:
    484     tool_input, kwargs = _prep_run_args(input, config, **kwargs)
--> 485     return self.run(tool_input, **kwargs)

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:688, in BaseTool.run(self, tool_input, verbose, start_color, color, callbacks, tags, metadata, run_name, run_id, config, tool_call_id, **kwargs)
    686 if error_to_raise:
    687     run_manager.on_tool_error(error_to_raise)
--> 688     raise error_to_raise
    689 output = _format_output(content, artifact, tool_call_id, self.name, status)
    690 run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:651, in BaseTool.run(self, tool_input, verbose, start_color, color, callbacks, tags, metadata, run_name, run_id, config, tool_call_id, **kwargs)
    649 context = copy_context()
    650 context.run(_set_config_context, child_config)
--> 651 tool_args, tool_kwargs = self._to_args_and_kwargs(tool_input)
    652 if signature(self._run).parameters.get("run_manager"):
    653     tool_kwargs["run_manager"] = run_manager

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:574, in BaseTool._to_args_and_kwargs(self, tool_input)
    573 def _to_args_and_kwargs(self, tool_input: Union[str, dict]) -> tuple[tuple, dict]:
--> 574     tool_input = self._parse_input(tool_input)
    575     # For backwards compatibility, if run_input is a string,
    576     # pass as a positional argument.
    577     if isinstance(tool_input, str):

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:516, in BaseTool._parse_input(self, tool_input)
    514 if input_args is not None:
    515     if issubclass(input_args, BaseModel):
--> 516         result = input_args.model_validate(tool_input)
    517         result_dict = result.model_dump()
    518     elif issubclass(input_args, BaseModelV1):

File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/pydantic/main.py:596, in BaseModel.model_validate(cls, obj, strict, from_attributes, context)
    594 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
    595 __tracebackhide__ = True
--> 596 return cls.__pydantic_validator__.validate_python(
    597     obj, strict=strict, from_attributes=from_attributes, context=context
    598 )

ValidationError: 1 validation error for get_weather_with_state
state
  Field required [type=missing, input_value={'location': 'San Francisco'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.9/v/missing

Description

When using state: Annotated[dict, InjectedState] in a tool argument and omitting it from the docstring, the state still throws a validation exception.

I copied the example code directly from the create react agent from scratch example, and added a 2nd tool which injects the state. There are no other differences in the tools, and you can verify the schema's with get_input_schema and tool_call_schema properly omit the state key as expected.
Screenshot 2024-09-30 at 14 45 35

However, when the model calls the tool, it throws a pydantic validation exception saying the state key is missing.

System Info

System Information

OS: Darwin
OS Version: Darwin Kernel Version 23.5.0: Wed May 1 20:14:38 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6020
Python Version: 3.11.7 (main, May 6 2024, 13:40:41) [Clang 15.0.0 (clang-1500.3.9.4)]

Package Information

langchain_core: 0.3.6
langsmith: 0.1.129
langchain_openai: 0.2.1
langgraph: 0.2.29

Optional packages not installed

langserve

Other Dependencies

httpx: 0.27.2
jsonpatch: 1.33
langgraph-checkpoint: 1.0.13
openai: 1.50.2
orjson: 3.10.7
packaging: 24.1
pydantic: 2.9.2
PyYAML: 6.0.2
requests: 2.32.3
tenacity: 8.5.0
tiktoken: 0.7.0
typing-extensions: 4.12.2

Originally created by @fletchertyler914 on GitHub (Sep 30, 2024). ### Checked other resources - [X] I added a very descriptive title to this issue. - [X] I searched the [LangGraph](https://langchain-ai.github.io/langgraph/)/LangChain documentation with the integrated search. - [X] I used the GitHub search to find a similar question and didn't find it. - [X] I am sure that this is a bug in LangGraph/LangChain rather than my code. - [X] I am sure this is better as an issue [rather than a GitHub discussion](https://github.com/langchain-ai/langgraph/discussions/new/choose), since this is a LangGraph bug and not a design question. ### Example Code ```python import json from typing import Annotated, Sequence, TypedDict from langchain_core.tools import tool from langchain_core.runnables import RunnableConfig from langchain_core.messages import ToolMessage, SystemMessage, BaseMessage from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, END from langgraph.prebuilt import InjectedState from langgraph.graph.message import add_messages class AgentState(TypedDict): """The state of the agent.""" messages: Annotated[Sequence[BaseMessage], add_messages] # Define a dummy get_weather tool with no state argument @tool(parse_docstring=True) def get_weather(location: str): """ Call to get the weather from a specific location. Args: location: The location to get the weather from. """ # This is a placeholder for the actual implementation # Don't let the LLM know this though 😊 if any([city in location.lower() for city in ['sf', 'san francisco']]): return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈." else: return f"I am not sure what the weather is in {location}" # ONLY add the (injected) state argument to this tool to show its broken @tool(parse_docstring=True) def get_weather_with_state( location: str, state: Annotated[dict, InjectedState] ): """ Call to get the BROKEN weather from a specific location. Use this if a user asks for BROKEN weather only, otherwise use get_weather. Args: location: The location to get the weather from. """ # This is a placeholder for the actual implementation # Don't let the LLM know this though 😊 print('get_weather state: ', state) if any([city in location.lower() for city in ['sf', 'san francisco']]): return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈." else: return f"I am not sure what the weather is in {location}" # Define our tools tools = [get_weather, get_weather_with_state] tools_by_name = {tool.name: tool for tool in tools} # Define our model model = ChatOpenAI(model="gpt-4o-mini") model = model.bind_tools(tools) # Define our tool node def tool_node(state: AgentState): outputs = [] for tool_call in state['messages'][-1].tool_calls: tool_result = tools_by_name[tool_call["name"]].invoke( tool_call["args"] ) outputs.append( ToolMessage( content=json.dumps(tool_result), name=tool_call["name"], tool_call_id=tool_call["id"], ) ) return {"messages": outputs} # Define the node that calls the model def call_model( state: AgentState, config: RunnableConfig, ): # this is similar to customizing the create_react_agent with state_modifier, but is a lot more flexible system_prompt = SystemMessage( "You are a helpful AI assistant, please respond to the users query to the best of your ability!") response = model.invoke([system_prompt] + state['messages'], config) # We return a list, because this will get added to the existing list return {"messages": [response]} # Define the conditional edge that determines whether to continue or not def should_continue(state: AgentState): messages = state["messages"] last_message = messages[-1] # If there is no function call, then we finish if not last_message.tool_calls: return "end" # Otherwise if there is, we continue else: return "continue" # Define a new graph workflow = StateGraph(AgentState) # Define the two nodes we will cycle between workflow.add_node("agent", call_model) workflow.add_node("tools", tool_node) # Set the entrypoint as `agent` # This means that this node is the first one called workflow.set_entry_point("agent") # We now add a conditional edge workflow.add_conditional_edges( # First, we define the start node. We use `agent`. # This means these are the edges taken after the `agent` node is called. "agent", # Next, we pass in the function that will determine which node is called next. should_continue, # Finally we pass in a mapping. # The keys are strings, and the values are other nodes. # END is a special node marking that the graph should finish. # What will happen is we will call `should_continue`, and then the output of that # will be matched against the keys in this mapping. # Based on which one it matches, that node will then be called. { # If `tools`, then we call the tool node. "continue": "tools", # Otherwise we finish. "end": END, }, ) # We now add a normal edge from `tools` to `agent`. # This means that after `tools` is called, `agent` node is called next. workflow.add_edge("tools", "agent") # Now we can compile and visualize our graph graph = workflow.compile() # Helper function for formatting the stream nicely def print_stream(stream): for s in stream: message = s["messages"][-1] if isinstance(message, tuple): print(message) else: message.pretty_print() # invoke the stateless (working) tool first to show it works inputs = {"messages": [("user", "what is the weather in sf")]} print_stream(graph.stream(inputs, stream_mode="values")) # invoke the stateful (non-working) tool next to show its broken (pydantic validation errors) inputs = {"messages": [("user", "what is the BROKEN weather in sf")]} print_stream(graph.stream(inputs, stream_mode="values")) ``` ### Error Message and Stack Trace (if applicable) ```shell --------------------------------------------------------------------------- ValidationError Traceback (most recent call last) Cell In[1], line 166 164 # invoke the stateful (non-working) tool next to show its broken (pydantic validation errors) 165 inputs = {"messages": [("user", "what is the BROKEN weather in sf")]} --> 166 print_stream(graph.stream(inputs, stream_mode="values")) Cell In[1], line 152 151 def print_stream(stream): --> 152 for s in stream: 153 message = s["messages"][-1] 154 if isinstance(message, tuple): File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/pregel/__init__.py:1298, in Pregel.stream(self, input, config, stream_mode, output_keys, interrupt_before, interrupt_after, debug, subgraphs) 1287 # Similarly to Bulk Synchronous Parallel / Pregel model 1288 # computation proceeds in steps, while there are channel updates 1289 # channel updates from step N are only visible in step N+1 1290 # channels are guaranteed to be immutable for the duration of the step, 1291 # with channel updates applied only at the transition between steps 1292 while loop.tick( 1293 input_keys=self.input_channels, 1294 interrupt_before=interrupt_before_, 1295 interrupt_after=interrupt_after_, 1296 manager=run_manager, 1297 ): -> 1298 for _ in runner.tick( 1299 loop.tasks.values(), 1300 timeout=self.step_timeout, 1301 retry_policy=self.retry_policy, 1302 get_waiter=get_waiter, 1303 ): 1304 # emit output 1305 yield from output() 1306 # emit output File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/pregel/runner.py:56, in PregelRunner.tick(self, tasks, reraise, timeout, retry_policy, get_waiter) 54 t = tasks[0] 55 try: ---> 56 run_with_retry(t, retry_policy) 57 self.commit(t, None) 58 except Exception as exc: File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/pregel/retry.py:29, in run_with_retry(task, retry_policy) 27 task.writes.clear() 28 # run the task ---> 29 task.proc.invoke(task.input, config) 30 # if successful, end 31 break File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/utils/runnable.py:405, in RunnableSeq.invoke(self, input, config, **kwargs) 403 context.run(_set_config_context, config) 404 if i == 0: --> 405 input = context.run(step.invoke, input, config, **kwargs) 406 else: 407 input = context.run(step.invoke, input, config) File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langgraph/utils/runnable.py:181, in RunnableCallable.invoke(self, input, config, **kwargs) 179 else: 180 context.run(_set_config_context, config) --> 181 ret = context.run(self.func, input, **kwargs) 182 if isinstance(ret, Runnable) and self.recurse: 183 return ret.invoke(input, config) Cell In[1], line 71 69 outputs = [] 70 for tool_call in state['messages'][-1].tool_calls: ---> 71 tool_result = tools_by_name[tool_call["name"]].invoke( 72 tool_call["args"] 73 ) 74 outputs.append( 75 ToolMessage( 76 content=json.dumps(tool_result), (...) 79 ) 80 ) 81 return {"messages": outputs} File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:485, in BaseTool.invoke(self, input, config, **kwargs) 478 def invoke( 479 self, 480 input: Union[str, dict, ToolCall], 481 config: Optional[RunnableConfig] = None, 482 **kwargs: Any, 483 ) -> Any: 484 tool_input, kwargs = _prep_run_args(input, config, **kwargs) --> 485 return self.run(tool_input, **kwargs) File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:688, in BaseTool.run(self, tool_input, verbose, start_color, color, callbacks, tags, metadata, run_name, run_id, config, tool_call_id, **kwargs) 686 if error_to_raise: 687 run_manager.on_tool_error(error_to_raise) --> 688 raise error_to_raise 689 output = _format_output(content, artifact, tool_call_id, self.name, status) 690 run_manager.on_tool_end(output, color=color, name=self.name, **kwargs) File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:651, in BaseTool.run(self, tool_input, verbose, start_color, color, callbacks, tags, metadata, run_name, run_id, config, tool_call_id, **kwargs) 649 context = copy_context() 650 context.run(_set_config_context, child_config) --> 651 tool_args, tool_kwargs = self._to_args_and_kwargs(tool_input) 652 if signature(self._run).parameters.get("run_manager"): 653 tool_kwargs["run_manager"] = run_manager File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:574, in BaseTool._to_args_and_kwargs(self, tool_input) 573 def _to_args_and_kwargs(self, tool_input: Union[str, dict]) -> tuple[tuple, dict]: --> 574 tool_input = self._parse_input(tool_input) 575 # For backwards compatibility, if run_input is a string, 576 # pass as a positional argument. 577 if isinstance(tool_input, str): File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/langchain_core/tools/base.py:516, in BaseTool._parse_input(self, tool_input) 514 if input_args is not None: 515 if issubclass(input_args, BaseModel): --> 516 result = input_args.model_validate(tool_input) 517 result_dict = result.model_dump() 518 elif issubclass(input_args, BaseModelV1): File ~/Library/Caches/pypoetry/virtualenvs/ask-harmony-FAOMPRZw-py3.11/lib/python3.11/site-packages/pydantic/main.py:596, in BaseModel.model_validate(cls, obj, strict, from_attributes, context) 594 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks 595 __tracebackhide__ = True --> 596 return cls.__pydantic_validator__.validate_python( 597 obj, strict=strict, from_attributes=from_attributes, context=context 598 ) ValidationError: 1 validation error for get_weather_with_state state Field required [type=missing, input_value={'location': 'San Francisco'}, input_type=dict] For further information visit https://errors.pydantic.dev/2.9/v/missing ``` ### Description When using `state: Annotated[dict, InjectedState]` in a tool argument and omitting it from the docstring, the `state` still throws a validation exception. I copied the example code directly from the [create react agent from scratch](https://langchain-ai.github.io/langgraph/how-tos/react-agent-from-scratch) example, and added a 2nd tool which injects the state. There are no other differences in the tools, and you can verify the schema's with `get_input_schema` and `tool_call_schema` properly omit the `state` key as expected. <img width="1168" alt="Screenshot 2024-09-30 at 14 45 35" src="https://github.com/user-attachments/assets/931f0ca2-20ed-4a12-a018-2f74bbd6628b"> However, when the model calls the tool, it throws a pydantic validation exception saying the `state` key is missing. ### System Info System Information ------------------ > OS: Darwin > OS Version: Darwin Kernel Version 23.5.0: Wed May 1 20:14:38 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6020 > Python Version: 3.11.7 (main, May 6 2024, 13:40:41) [Clang 15.0.0 (clang-1500.3.9.4)] Package Information ------------------- > langchain_core: 0.3.6 > langsmith: 0.1.129 > langchain_openai: 0.2.1 > langgraph: 0.2.29 Optional packages not installed ------------------------------- > langserve Other Dependencies ------------------ > httpx: 0.27.2 > jsonpatch: 1.33 > langgraph-checkpoint: 1.0.13 > openai: 1.50.2 > orjson: 3.10.7 > packaging: 24.1 > pydantic: 2.9.2 > PyYAML: 6.0.2 > requests: 2.32.3 > tenacity: 8.5.0 > tiktoken: 0.7.0 > typing-extensions: 4.12.2
yindo closed this issue 2026-02-20 17:33:43 -05:00
Author
Owner

@fletchertyler914 commented on GitHub (Sep 30, 2024):

Ah, this was my mistake. I didn't realize the prebuilt ToolNode was doing this behind the scenes!

@fletchertyler914 commented on GitHub (Sep 30, 2024): Ah, this was my mistake. I didn't realize the prebuilt `ToolNode` was doing this behind the scenes!
Author
Owner

@mezeirobert0 commented on GitHub (Nov 5, 2025):

So how did you solve the problem?

@mezeirobert0 commented on GitHub (Nov 5, 2025): So how did you solve the problem?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#250