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.
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/PKG-INFO +1 -1
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/chat_agent_executor.py +35 -38
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/tool_node.py +16 -29
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/tool_validator.py +2 -3
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/pyproject.toml +1 -1
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_deprecation.py +8 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_react_agent.py +185 -1
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/uv.lock +2 -2
- langgraph_prebuilt-0.6.2/langgraph/prebuilt/_internal.py +0 -26
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/.gitignore +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/LICENSE +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/Makefile +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/README.md +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/__init__.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/interrupt.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/py.typed +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/__init__.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/__snapshots__/test_react_agent_graph.ambr +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/any_str.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/compose-postgres.yml +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/conftest.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/conftest_checkpointer.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/conftest_store.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/memory_assert.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/messages.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/model.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_react_agent_graph.py +0 -0
- {langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/tests/test_tool_node.py +0 -0
- {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.
|
|
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
|
{langgraph_prebuilt-0.6.2 → langgraph_prebuilt-0.6.4}/langgraph/prebuilt/chat_agent_executor.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
506
|
-
response = await self.tools_by_name[call["name"]].ainvoke(
|
|
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
|
-
# (
|
|
511
|
-
# (
|
|
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 = [
|
|
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)
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
51
|
+
It can be used either in StateGraph with a "messages" key.
|
|
53
52
|
|
|
54
53
|
!!! note
|
|
55
54
|
|
|
@@ -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.
|
|
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.
|
|
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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|