langgraph-prebuilt 0.6.2__tar.gz → 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/PKG-INFO +1 -1
  2. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/chat_agent_executor.py +35 -38
  3. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/tool_node.py +16 -29
  4. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/tool_validator.py +2 -3
  5. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/pyproject.toml +1 -1
  6. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_deprecation.py +8 -0
  7. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_react_agent.py +185 -1
  8. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/uv.lock +2 -2
  9. langgraph_prebuilt-0.6.2/langgraph/prebuilt/_internal.py +0 -26
  10. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/.gitignore +0 -0
  11. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/LICENSE +0 -0
  12. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/Makefile +0 -0
  13. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/README.md +0 -0
  14. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/__init__.py +0 -0
  15. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/interrupt.py +0 -0
  16. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/py.typed +0 -0
  17. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/__init__.py +0 -0
  18. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/__snapshots__/test_react_agent_graph.ambr +0 -0
  19. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/any_str.py +0 -0
  20. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/compose-postgres.yml +0 -0
  21. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/conftest.py +0 -0
  22. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/conftest_checkpointer.py +0 -0
  23. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/conftest_store.py +0 -0
  24. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/memory_assert.py +0 -0
  25. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/messages.py +0 -0
  26. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/model.py +0 -0
  27. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_react_agent_graph.py +0 -0
  28. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_tool_node.py +0 -0
  29. {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_validation_node.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-prebuilt
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Library with high-level APIs for creating and executing LangGraph agents and tools.
5
5
  Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
6
6
  License-Expression: MIT
@@ -34,7 +34,7 @@ from langchain_core.runnables import (
34
34
  )
35
35
  from langchain_core.tools import BaseTool
36
36
  from pydantic import BaseModel
37
- from typing_extensions import Annotated, TypedDict
37
+ from typing_extensions import Annotated, NotRequired, TypedDict
38
38
 
39
39
  from langgraph._internal._runnable import RunnableCallable, RunnableLike
40
40
  from langgraph._internal._typing import MISSING
@@ -42,8 +42,7 @@ from langgraph.errors import ErrorCode, create_error_message
42
42
  from langgraph.graph import END, StateGraph
43
43
  from langgraph.graph.message import add_messages
44
44
  from langgraph.graph.state import CompiledStateGraph
45
- from langgraph.managed import IsLastStep, RemainingSteps
46
- from langgraph.prebuilt._internal import ToolCallWithContext
45
+ from langgraph.managed import RemainingSteps
47
46
  from langgraph.prebuilt.tool_node import ToolNode
48
47
  from langgraph.runtime import Runtime
49
48
  from langgraph.store.base import BaseStore
@@ -65,9 +64,7 @@ class AgentState(TypedDict):
65
64
 
66
65
  messages: Annotated[Sequence[BaseMessage], add_messages]
67
66
 
68
- is_last_step: IsLastStep
69
-
70
- remaining_steps: RemainingSteps
67
+ remaining_steps: NotRequired[RemainingSteps]
71
68
 
72
69
 
73
70
  class AgentStatePydantic(BaseModel):
@@ -254,6 +251,13 @@ def create_react_agent(
254
251
  LanguageModelLike,
255
252
  Callable[[StateSchema, Runtime[ContextT]], BaseChatModel],
256
253
  Callable[[StateSchema, Runtime[ContextT]], Awaitable[BaseChatModel]],
254
+ Callable[
255
+ [StateSchema, Runtime[ContextT]], Runnable[LanguageModelInput, BaseMessage]
256
+ ],
257
+ Callable[
258
+ [StateSchema, Runtime[ContextT]],
259
+ Awaitable[Runnable[LanguageModelInput, BaseMessage]],
260
+ ],
257
261
  ],
258
262
  tools: Union[Sequence[Union[BaseTool, Callable, dict[str, Any]]], ToolNode],
259
263
  *,
@@ -287,6 +291,9 @@ def create_react_agent(
287
291
  - **Dynamic model**: A callable with signature
288
292
  `(state, runtime) -> BaseChatModel` that returns different models
289
293
  based on runtime context
294
+ If the model has tools bound via `.bind_tools()` or other configurations,
295
+ the return type should be a Runnable[LanguageModelInput, BaseMessage]
296
+ Coroutines are also supported, allowing for asynchronous model selection.
290
297
 
291
298
  Dynamic functions receive graph state and runtime, enabling
292
299
  context-dependent model selection. Must return a `BaseChatModel`
@@ -466,6 +473,11 @@ def create_react_agent(
466
473
  if context_schema is None:
467
474
  context_schema = config_schema
468
475
 
476
+ if len(deprecated_kwargs) > 0:
477
+ raise TypeError(
478
+ f"create_react_agent() got unexpected keyword arguments: {deprecated_kwargs}"
479
+ )
480
+
469
481
  if version not in ("v1", "v2"):
470
482
  raise ValueError(
471
483
  f"Invalid version {version}. Supported versions are 'v1' and 'v2'."
@@ -561,16 +573,13 @@ def create_react_agent(
561
573
  else False
562
574
  )
563
575
  remaining_steps = _get_state_value(state, "remaining_steps", None)
564
- is_last_step = _get_state_value(state, "is_last_step", False)
565
- return (
566
- (remaining_steps is None and is_last_step and has_tool_calls)
567
- or (
568
- remaining_steps is not None
569
- and remaining_steps < 1
570
- and all_tools_return_direct
571
- )
572
- or (remaining_steps is not None and remaining_steps < 2 and has_tool_calls)
573
- )
576
+ if remaining_steps is not None:
577
+ if remaining_steps < 1 and all_tools_return_direct:
578
+ return True
579
+ elif remaining_steps < 2 and has_tool_calls:
580
+ return True
581
+
582
+ return False
574
583
 
575
584
  def _get_model_input_state(state: StateSchema) -> StateSchema:
576
585
  if pre_model_hook is not None:
@@ -785,17 +794,11 @@ def create_react_agent(
785
794
  elif version == "v2":
786
795
  if post_model_hook is not None:
787
796
  return "post_model_hook"
788
- return [
789
- Send(
790
- "tools",
791
- ToolCallWithContext(
792
- __type="tool_call_with_context",
793
- tool_call=tool_call,
794
- state=state,
795
- ),
796
- )
797
- for tool_call in last_message.tool_calls
797
+ tool_calls = [
798
+ tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
799
+ for call in last_message.tool_calls
798
800
  ]
801
+ return [Send("tools", [tool_call]) for tool_call in tool_calls]
799
802
 
800
803
  # Define a new graph
801
804
  workflow = StateGraph(
@@ -876,17 +879,11 @@ def create_react_agent(
876
879
  ]
877
880
 
878
881
  if pending_tool_calls:
879
- return [
880
- Send(
881
- "tools",
882
- ToolCallWithContext(
883
- __type="tool_call_with_context",
884
- tool_call=tool_call,
885
- state=state,
886
- ),
887
- )
888
- for tool_call in pending_tool_calls
882
+ pending_tool_calls = [
883
+ tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
884
+ for call in pending_tool_calls
889
885
  ]
886
+ return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]
890
887
  elif isinstance(messages[-1], ToolMessage):
891
888
  return entrypoint
892
889
  elif response_format is not None:
@@ -896,13 +893,13 @@ def create_react_agent(
896
893
 
897
894
  workflow.add_conditional_edges(
898
895
  "post_model_hook",
899
- post_model_hook_router, # type: ignore[arg-type]
896
+ post_model_hook_router,
900
897
  path_map=post_model_hook_paths,
901
898
  )
902
899
 
903
900
  workflow.add_conditional_edges(
904
901
  "agent",
905
- should_continue, # type: ignore[arg-type]
902
+ should_continue,
906
903
  path_map=agent_paths,
907
904
  )
908
905
 
@@ -74,7 +74,6 @@ from typing_extensions import Annotated, get_args, get_origin
74
74
  from langgraph._internal._runnable import RunnableCallable
75
75
  from langgraph.errors import GraphBubbleUp
76
76
  from langgraph.graph.message import REMOVE_ALL_MESSAGES
77
- from langgraph.prebuilt._internal import ToolCallWithContext
78
77
  from langgraph.store.base import BaseStore
79
78
  from langgraph.types import Command, Send
80
79
 
@@ -361,8 +360,7 @@ class ToolNode(RunnableCallable):
361
360
  *,
362
361
  store: Optional[BaseStore],
363
362
  ) -> Any:
364
- tool_calls, input_type = self._parse_input(input)
365
- tool_calls = [self.inject_tool_args(call, input, store) for call in tool_calls]
363
+ tool_calls, input_type = self._parse_input(input, store)
366
364
  config_list = get_config_list(config, len(tool_calls))
367
365
  input_types = [input_type] * len(tool_calls)
368
366
  with get_executor_for_config(config) as executor:
@@ -383,8 +381,7 @@ class ToolNode(RunnableCallable):
383
381
  *,
384
382
  store: Optional[BaseStore],
385
383
  ) -> Any:
386
- tool_calls, input_type = self._parse_input(input)
387
- tool_calls = [self.inject_tool_args(call, input, store) for call in tool_calls]
384
+ tool_calls, input_type = self._parse_input(input, store)
388
385
  outputs = await asyncio.gather(
389
386
  *(self._arun_one(call, input_type, config) for call in tool_calls)
390
387
  )
@@ -502,13 +499,14 @@ class ToolNode(RunnableCallable):
502
499
  return invalid_tool_message
503
500
 
504
501
  try:
505
- input = {**call, **{"type": "tool_call"}}
506
- response = await self.tools_by_name[call["name"]].ainvoke(input, config)
502
+ call_args = {**call, **{"type": "tool_call"}}
503
+ response = await self.tools_by_name[call["name"]].ainvoke(call_args, config)
507
504
 
508
505
  # GraphInterrupt is a special exception that will always be raised.
509
- # It can be triggered in the following scenarios:
510
- # (1) a NodeInterrupt is raised inside a tool
511
- # (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
506
+ # It can be triggered in the following scenarios,
507
+ # Where GraphInterrupt(GraphBubbleUp) is raised from an `interrupt` invocation most commonly:
508
+ # (1) a GraphInterrupt is raised inside a tool
509
+ # (2) a GraphInterrupt is raised inside a graph node for a graph called as a tool
512
510
  # (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
513
511
  # (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
514
512
  except GraphBubbleUp as e:
@@ -555,6 +553,7 @@ class ToolNode(RunnableCallable):
555
553
  dict[str, Any],
556
554
  BaseModel,
557
555
  ],
556
+ store: Optional[BaseStore],
558
557
  ) -> Tuple[list[ToolCall], Literal["list", "dict", "tool_calls"]]:
559
558
  input_type: Literal["list", "dict", "tool_calls"]
560
559
  if isinstance(input, list):
@@ -565,15 +564,6 @@ class ToolNode(RunnableCallable):
565
564
  else:
566
565
  input_type = "list"
567
566
  messages = input
568
- elif (
569
- isinstance(input, dict) and input.get("__type") == "tool_call_with_context"
570
- ):
571
- # mypy will not be able to type narrow correctly since the signature
572
- # for input contains dict[str, Any]. We'd need to type dict[str, Any]
573
- # before we can apply correct typing.
574
- input = cast(ToolCallWithContext, input) # type: ignore[assignment]
575
- input_type = "tool_calls"
576
- return [input["tool_call"]], input_type
577
567
  elif isinstance(input, dict) and (messages := input.get(self.messages_key, [])):
578
568
  input_type = "dict"
579
569
  elif messages := getattr(input, self.messages_key, []):
@@ -589,7 +579,10 @@ class ToolNode(RunnableCallable):
589
579
  except StopIteration:
590
580
  raise ValueError("No AIMessage found in input")
591
581
 
592
- tool_calls = [call for call in latest_ai_message.tool_calls]
582
+ tool_calls = [
583
+ self.inject_tool_args(call, input, store)
584
+ for call in latest_ai_message.tool_calls
585
+ ]
593
586
  return tool_calls, input_type
594
587
 
595
588
  def _validate_tool_call(self, call: ToolCall) -> Optional[ToolMessage]:
@@ -632,19 +625,14 @@ class ToolNode(RunnableCallable):
632
625
  err_msg += f" State should contain fields {required_fields_str}."
633
626
  raise ValueError(err_msg)
634
627
 
635
- if isinstance(input, dict) and input.get("__type") == "tool_call_with_context":
636
- state = input["state"]
637
- else:
638
- state = input
639
-
640
- if isinstance(state, dict):
628
+ if isinstance(input, dict):
641
629
  tool_state_args = {
642
- tool_arg: state[state_field] if state_field else state
630
+ tool_arg: input[state_field] if state_field else input
643
631
  for tool_arg, state_field in state_args.items()
644
632
  }
645
633
  else:
646
634
  tool_state_args = {
647
- tool_arg: getattr(state, state_field) if state_field else state
635
+ tool_arg: getattr(input, state_field) if state_field else input
648
636
  for tool_arg, state_field in state_args.items()
649
637
  }
650
638
 
@@ -802,7 +790,6 @@ def tools_condition(
802
790
 
803
791
  Args:
804
792
  state: The current graph state to examine for tool calls. Supported formats:
805
- - List of messages (for MessageGraph)
806
793
  - Dictionary containing a messages key (for StateGraph)
807
794
  - BaseModel instance with a messages attribute
808
795
  messages_key: The key or attribute name containing the message list in the state.
@@ -2,8 +2,7 @@
2
2
  in a langchain graph. It applies a pydantic schema to tool_calls in the models' outputs,
3
3
  and returns a ToolMessage with the validated content. If the schema is not valid, it
4
4
  returns a ToolMessage with the error message. The ValidationNode can be used in a
5
- StateGraph with a "messages" key or in a MessageGraph. If multiple tool calls are
6
- requested, they will be run in parallel.
5
+ StateGraph with a "messages" key. If multiple tool calls are requested, they will be run in parallel.
7
6
  """
8
7
 
9
8
  from typing import (
@@ -49,7 +48,7 @@ def _default_format_error(
49
48
  class ValidationNode(RunnableCallable):
50
49
  """A node that validates all tools requests from the last AIMessage.
51
50
 
52
- It can be used either in StateGraph with a "messages" key or in MessageGraph.
51
+ It can be used either in StateGraph with a "messages" key.
53
52
 
54
53
  !!! note
55
54
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-prebuilt"
7
- version = "0.6.2"
7
+ version = "0.6.4"
8
8
  description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
9
9
  authors = []
10
10
  requires-python = ">=3.9"
@@ -31,3 +31,11 @@ def test_config_schema_deprecation() -> None:
31
31
  match="`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead.",
32
32
  ):
33
33
  assert agent.get_config_jsonschema() is not None
34
+
35
+
36
+ def test_extra_kwargs_deprecation() -> None:
37
+ with pytest.raises(
38
+ TypeError,
39
+ match="create_react_agent\(\) got unexpected keyword arguments: \{'extra': 'extra'\}",
40
+ ):
41
+ create_react_agent(FakeToolCallingModel(), [], extra="extra")
@@ -24,7 +24,7 @@ from langchain_core.messages import (
24
24
  ToolCall,
25
25
  ToolMessage,
26
26
  )
27
- from langchain_core.runnables import RunnableLambda
27
+ from langchain_core.runnables import RunnableConfig, RunnableLambda
28
28
  from langchain_core.tools import InjectedToolCallId, ToolException
29
29
  from langchain_core.tools import tool as dec_tool
30
30
  from pydantic import BaseModel, Field
@@ -1236,6 +1236,190 @@ def test_tool_node_stream_writer() -> None:
1236
1236
  ]
1237
1237
 
1238
1238
 
1239
+ @pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
1240
+ def test_react_agent_subgraph_streaming_sync(version: Literal["v1", "v2"]) -> None:
1241
+ """Test React agent streaming when used as a subgraph node sync version"""
1242
+
1243
+ @dec_tool
1244
+ def get_weather(city: str) -> str:
1245
+ """Get the weather of a city."""
1246
+ return f"The weather of {city} is sunny."
1247
+
1248
+ # Create a React agent
1249
+ model = FakeToolCallingModel(
1250
+ tool_calls=[
1251
+ [{"args": {"city": "Tokyo"}, "id": "1", "name": "get_weather"}],
1252
+ [],
1253
+ ]
1254
+ )
1255
+
1256
+ agent = create_react_agent(
1257
+ model,
1258
+ tools=[get_weather],
1259
+ prompt="You are a helpful travel assistant.",
1260
+ version=version,
1261
+ )
1262
+
1263
+ # Create a subgraph that uses the React agent as a node
1264
+ def react_agent_node(state: MessagesState, config: RunnableConfig) -> MessagesState:
1265
+ """Node that runs the React agent and collects streaming output."""
1266
+ collected_content = ""
1267
+
1268
+ # Stream the agent output and collect content
1269
+ for msg_chunk, msg_metadata in agent.stream(
1270
+ {"messages": [("user", state["messages"][-1].content)]},
1271
+ config,
1272
+ stream_mode="messages",
1273
+ ):
1274
+ if hasattr(msg_chunk, "content") and msg_chunk.content:
1275
+ collected_content += msg_chunk.content
1276
+
1277
+ return {"messages": [("assistant", collected_content)]}
1278
+
1279
+ # Create the main workflow with the React agent as a subgraph node
1280
+ workflow = StateGraph(MessagesState)
1281
+ workflow.add_node("react_agent", react_agent_node)
1282
+ workflow.add_edge(START, "react_agent")
1283
+ workflow.add_edge("react_agent", "__end__")
1284
+ compiled_workflow = workflow.compile()
1285
+
1286
+ # Test the streaming functionality
1287
+ result = compiled_workflow.invoke(
1288
+ {"messages": [("user", "What is the weather in Tokyo?")]}
1289
+ )
1290
+
1291
+ # Verify the result contains expected structure
1292
+ assert len(result["messages"]) == 2
1293
+ assert result["messages"][0].content == "What is the weather in Tokyo?"
1294
+ assert "assistant" in str(result["messages"][1])
1295
+
1296
+ # Test streaming with subgraphs = True
1297
+ result = compiled_workflow.invoke(
1298
+ {"messages": [("user", "What is the weather in Tokyo?")]},
1299
+ subgraphs=True,
1300
+ )
1301
+ assert len(result["messages"]) == 2
1302
+
1303
+ events = []
1304
+ for event in compiled_workflow.stream(
1305
+ {"messages": [("user", "What is the weather in Tokyo?")]},
1306
+ stream_mode="messages",
1307
+ subgraphs=False,
1308
+ ):
1309
+ events.append(event)
1310
+
1311
+ assert len(events) == 0
1312
+
1313
+ events = []
1314
+ for event in compiled_workflow.stream(
1315
+ {"messages": [("user", "What is the weather in Tokyo?")]},
1316
+ stream_mode="messages",
1317
+ subgraphs=True,
1318
+ ):
1319
+ events.append(event)
1320
+
1321
+ assert len(events) == 3
1322
+ namespace, (msg, metadata) = events[0]
1323
+ # FakeToolCallingModel returns a single AIMessage with tool calls
1324
+ # The content of the AIMessage reflects the input message
1325
+ assert msg.content.startswith("You are a helpful travel assistant")
1326
+ namespace, (msg, metadata) = events[1] # ToolMessage
1327
+ assert msg.content.startswith("The weather of Tokyo is sunny.")
1328
+
1329
+
1330
+ @pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
1331
+ async def test_react_agent_subgraph_streaming(version: Literal["v1", "v2"]) -> None:
1332
+ """Test React agent streaming when used as a subgraph node."""
1333
+
1334
+ @dec_tool
1335
+ def get_weather(city: str) -> str:
1336
+ """Get the weather of a city."""
1337
+ return f"The weather of {city} is sunny."
1338
+
1339
+ # Create a React agent
1340
+ model = FakeToolCallingModel(
1341
+ tool_calls=[
1342
+ [{"args": {"city": "Tokyo"}, "id": "1", "name": "get_weather"}],
1343
+ [],
1344
+ ]
1345
+ )
1346
+
1347
+ agent = create_react_agent(
1348
+ model,
1349
+ tools=[get_weather],
1350
+ prompt="You are a helpful travel assistant.",
1351
+ version=version,
1352
+ )
1353
+
1354
+ # Create a subgraph that uses the React agent as a node
1355
+ async def react_agent_node(
1356
+ state: MessagesState, config: RunnableConfig
1357
+ ) -> MessagesState:
1358
+ """Node that runs the React agent and collects streaming output."""
1359
+ collected_content = ""
1360
+
1361
+ # Stream the agent output and collect content
1362
+ async for msg_chunk, msg_metadata in agent.astream(
1363
+ {"messages": [("user", state["messages"][-1].content)]},
1364
+ config,
1365
+ stream_mode="messages",
1366
+ ):
1367
+ if hasattr(msg_chunk, "content") and msg_chunk.content:
1368
+ collected_content += msg_chunk.content
1369
+
1370
+ return {"messages": [("assistant", collected_content)]}
1371
+
1372
+ # Create the main workflow with the React agent as a subgraph node
1373
+ workflow = StateGraph(MessagesState)
1374
+ workflow.add_node("react_agent", react_agent_node)
1375
+ workflow.add_edge(START, "react_agent")
1376
+ workflow.add_edge("react_agent", "__end__")
1377
+ compiled_workflow = workflow.compile()
1378
+
1379
+ # Test the streaming functionality
1380
+ result = await compiled_workflow.ainvoke(
1381
+ {"messages": [("user", "What is the weather in Tokyo?")]}
1382
+ )
1383
+
1384
+ # Verify the result contains expected structure
1385
+ assert len(result["messages"]) == 2
1386
+ assert result["messages"][0].content == "What is the weather in Tokyo?"
1387
+ assert "assistant" in str(result["messages"][1])
1388
+
1389
+ # Test streaming with subgraphs = True
1390
+ result = await compiled_workflow.ainvoke(
1391
+ {"messages": [("user", "What is the weather in Tokyo?")]},
1392
+ subgraphs=True,
1393
+ )
1394
+ assert len(result["messages"]) == 2
1395
+
1396
+ events = []
1397
+ async for event in compiled_workflow.astream(
1398
+ {"messages": [("user", "What is the weather in Tokyo?")]},
1399
+ stream_mode="messages",
1400
+ subgraphs=False,
1401
+ ):
1402
+ events.append(event)
1403
+
1404
+ assert len(events) == 0
1405
+
1406
+ events = []
1407
+ async for event in compiled_workflow.astream(
1408
+ {"messages": [("user", "What is the weather in Tokyo?")]},
1409
+ stream_mode="messages",
1410
+ subgraphs=True,
1411
+ ):
1412
+ events.append(event)
1413
+
1414
+ assert len(events) == 3
1415
+ namespace, (msg, metadata) = events[0]
1416
+ # FakeToolCallingModel returns a single AIMessage with tool calls
1417
+ # The content of the AIMessage reflects the input message
1418
+ assert msg.content.startswith("You are a helpful travel assistant")
1419
+ namespace, (msg, metadata) = events[1] # ToolMessage
1420
+ assert msg.content.startswith("The weather of Tokyo is sunny.")
1421
+
1422
+
1239
1423
  @pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
1240
1424
  def test_tool_node_node_interrupt(
1241
1425
  sync_checkpointer: BaseCheckpointSaver, version: str
@@ -316,7 +316,7 @@ wheels = [
316
316
 
317
317
  [[package]]
318
318
  name = "langgraph"
319
- version = "0.6.1"
319
+ version = "0.6.4"
320
320
  source = { editable = "../langgraph" }
321
321
  dependencies = [
322
322
  { name = "langchain-core" },
@@ -460,7 +460,7 @@ dev = [
460
460
 
461
461
  [[package]]
462
462
  name = "langgraph-prebuilt"
463
- version = "0.6.2"
463
+ version = "0.6.4"
464
464
  source = { editable = "." }
465
465
  dependencies = [
466
466
  { name = "langchain-core" },
@@ -1,26 +0,0 @@
1
- from typing import Any, Literal, TypedDict
2
-
3
- from langchain_core.messages import ToolCall
4
-
5
-
6
- class ToolCallWithContext(TypedDict):
7
- """ToolCall with additional context for graph state.
8
-
9
- This is an internal data-structure meant to help the ToolNode accept
10
- tools calls with additional context (e.g. state) when dispatched using the
11
- `Send` API.
12
-
13
- The Send API is used in create_react_agent to be able to distribute the tool
14
- calls in parallel and support human-in-the-loop workflows where graph execution
15
- may be paused for an indefinite time.
16
- """
17
-
18
- tool_call: ToolCall
19
- __type: Literal["tool_call_with_context"]
20
- """Type to parameterize the payload.
21
-
22
- Using "__" as a prefix to be defensive against potential name collisions with
23
- regular user state.
24
- """
25
- state: Any
26
- """The state is provided as additional context."""