langchain 1.0.0a14__py3-none-any.whl → 1.0.0rc1__py3-none-any.whl
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.
Potentially problematic release.
This version of langchain might be problematic. Click here for more details.
- langchain/__init__.py +1 -1
- langchain/agents/__init__.py +7 -1
- langchain/agents/factory.py +74 -49
- langchain/agents/middleware/__init__.py +15 -6
- langchain/agents/middleware/context_editing.py +30 -2
- langchain/agents/middleware/human_in_the_loop.py +24 -20
- langchain/agents/middleware/model_call_limit.py +42 -9
- langchain/agents/middleware/model_fallback.py +36 -3
- langchain/agents/middleware/pii.py +7 -7
- langchain/agents/middleware/{planning.py → todo.py} +18 -5
- langchain/agents/middleware/tool_call_limit.py +89 -16
- langchain/agents/middleware/tool_emulator.py +2 -2
- langchain/agents/middleware/tool_retry.py +384 -0
- langchain/agents/middleware/types.py +111 -63
- langchain/agents/structured_output.py +29 -25
- langchain/chat_models/__init__.py +7 -1
- langchain/chat_models/base.py +98 -108
- langchain/embeddings/__init__.py +7 -1
- langchain/embeddings/base.py +1 -1
- langchain/messages/__init__.py +10 -1
- langchain/tools/__init__.py +9 -3
- langchain/tools/tool_node.py +288 -94
- langchain-1.0.0rc1.dist-info/METADATA +85 -0
- langchain-1.0.0rc1.dist-info/RECORD +30 -0
- langchain/agents/middleware/prompt_caching.py +0 -89
- langchain-1.0.0a14.dist-info/METADATA +0 -125
- langchain-1.0.0a14.dist-info/RECORD +0 -30
- {langchain-1.0.0a14.dist-info → langchain-1.0.0rc1.dist-info}/WHEEL +0 -0
- {langchain-1.0.0a14.dist-info → langchain-1.0.0rc1.dist-info}/licenses/LICENSE +0 -0
langchain/tools/tool_node.py
CHANGED
|
@@ -46,9 +46,10 @@ from typing import (
|
|
|
46
46
|
TYPE_CHECKING,
|
|
47
47
|
Annotated,
|
|
48
48
|
Any,
|
|
49
|
+
Generic,
|
|
49
50
|
Literal,
|
|
50
|
-
Optional,
|
|
51
51
|
TypedDict,
|
|
52
|
+
TypeVar,
|
|
52
53
|
Union,
|
|
53
54
|
cast,
|
|
54
55
|
get_args,
|
|
@@ -65,6 +66,7 @@ from langchain_core.messages import (
|
|
|
65
66
|
convert_to_messages,
|
|
66
67
|
)
|
|
67
68
|
from langchain_core.runnables.config import (
|
|
69
|
+
RunnableConfig,
|
|
68
70
|
get_config_list,
|
|
69
71
|
get_executor_for_config,
|
|
70
72
|
)
|
|
@@ -78,15 +80,18 @@ from langchain_core.tools.base import (
|
|
|
78
80
|
from langgraph._internal._runnable import RunnableCallable
|
|
79
81
|
from langgraph.errors import GraphBubbleUp
|
|
80
82
|
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
|
81
|
-
from langgraph.
|
|
82
|
-
from langgraph.types import Command, Send
|
|
83
|
+
from langgraph.store.base import BaseStore # noqa: TC002
|
|
84
|
+
from langgraph.types import Command, Send, StreamWriter
|
|
83
85
|
from pydantic import BaseModel, ValidationError
|
|
86
|
+
from typing_extensions import Unpack
|
|
84
87
|
|
|
85
88
|
if TYPE_CHECKING:
|
|
86
89
|
from collections.abc import Sequence
|
|
87
90
|
|
|
88
|
-
from
|
|
89
|
-
|
|
91
|
+
from langgraph.runtime import Runtime
|
|
92
|
+
|
|
93
|
+
StateT = TypeVar("StateT")
|
|
94
|
+
ContextT = TypeVar("ContextT")
|
|
90
95
|
|
|
91
96
|
INVALID_TOOL_NAME_ERROR_TEMPLATE = (
|
|
92
97
|
"Error: {requested_tool} is not a valid tool, try one of [{available_tools}]."
|
|
@@ -104,21 +109,55 @@ TOOL_INVOCATION_ERROR_TEMPLATE = (
|
|
|
104
109
|
)
|
|
105
110
|
|
|
106
111
|
|
|
112
|
+
class _ToolCallRequestOverrides(TypedDict, total=False):
|
|
113
|
+
"""Possible overrides for ToolCallRequest.override() method."""
|
|
114
|
+
|
|
115
|
+
tool_call: ToolCall
|
|
116
|
+
|
|
117
|
+
|
|
107
118
|
@dataclass()
|
|
108
119
|
class ToolCallRequest:
|
|
109
120
|
"""Tool execution request passed to tool call interceptors.
|
|
110
121
|
|
|
111
122
|
Attributes:
|
|
112
123
|
tool_call: Tool call dict with name, args, and id from model output.
|
|
113
|
-
tool: BaseTool instance to be invoked
|
|
124
|
+
tool: BaseTool instance to be invoked, or None if tool is not
|
|
125
|
+
registered with the ToolNode. When tool is None, interceptors can
|
|
126
|
+
handle the request without validation. If the interceptor calls execute(),
|
|
127
|
+
validation will occur and raise an error for unregistered tools.
|
|
114
128
|
state: Agent state (dict, list, or BaseModel).
|
|
115
129
|
runtime: LangGraph runtime context (optional, None if outside graph).
|
|
116
130
|
"""
|
|
117
131
|
|
|
118
132
|
tool_call: ToolCall
|
|
119
|
-
tool: BaseTool
|
|
133
|
+
tool: BaseTool | None
|
|
120
134
|
state: Any
|
|
121
|
-
runtime:
|
|
135
|
+
runtime: ToolRuntime
|
|
136
|
+
|
|
137
|
+
def override(self, **overrides: Unpack[_ToolCallRequestOverrides]) -> ToolCallRequest:
|
|
138
|
+
"""Replace the request with a new request with the given overrides.
|
|
139
|
+
|
|
140
|
+
Returns a new `ToolCallRequest` instance with the specified attributes replaced.
|
|
141
|
+
This follows an immutable pattern, leaving the original request unchanged.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
**overrides: Keyword arguments for attributes to override. Supported keys:
|
|
145
|
+
- tool_call: Tool call dict with name, args, and id
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
New ToolCallRequest instance with specified overrides applied.
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
```python
|
|
152
|
+
# Modify tool call arguments without mutating original
|
|
153
|
+
modified_call = {**request.tool_call, "args": {"value": 10}}
|
|
154
|
+
new_request = request.override(tool_call=modified_call)
|
|
155
|
+
|
|
156
|
+
# Override multiple attributes
|
|
157
|
+
new_request = request.override(tool_call=modified_call, state=new_state)
|
|
158
|
+
```
|
|
159
|
+
"""
|
|
160
|
+
return replace(self, **overrides)
|
|
122
161
|
|
|
123
162
|
|
|
124
163
|
ToolCallWrapper = Callable[
|
|
@@ -151,12 +190,15 @@ Examples:
|
|
|
151
190
|
|
|
152
191
|
Modify request before execution:
|
|
153
192
|
|
|
193
|
+
```python
|
|
154
194
|
def handler(request, execute):
|
|
155
195
|
request.tool_call["args"]["value"] *= 2
|
|
156
196
|
return execute(request)
|
|
197
|
+
```
|
|
157
198
|
|
|
158
199
|
Retry on error (execute multiple times):
|
|
159
200
|
|
|
201
|
+
```python
|
|
160
202
|
def handler(request, execute):
|
|
161
203
|
for attempt in range(3):
|
|
162
204
|
try:
|
|
@@ -167,9 +209,11 @@ Examples:
|
|
|
167
209
|
if attempt == 2:
|
|
168
210
|
raise
|
|
169
211
|
return result
|
|
212
|
+
```
|
|
170
213
|
|
|
171
214
|
Conditional retry based on response:
|
|
172
215
|
|
|
216
|
+
```python
|
|
173
217
|
def handler(request, execute):
|
|
174
218
|
for attempt in range(3):
|
|
175
219
|
result = execute(request)
|
|
@@ -178,15 +222,18 @@ Examples:
|
|
|
178
222
|
if attempt < 2:
|
|
179
223
|
continue
|
|
180
224
|
return result
|
|
225
|
+
```
|
|
181
226
|
|
|
182
227
|
Cache/short-circuit without calling execute:
|
|
183
228
|
|
|
229
|
+
```python
|
|
184
230
|
def handler(request, execute):
|
|
185
231
|
if cached := get_cache(request):
|
|
186
232
|
return ToolMessage(content=cached, tool_call_id=request.tool_call["id"])
|
|
187
233
|
result = execute(request)
|
|
188
234
|
save_cache(request, result)
|
|
189
235
|
return result
|
|
236
|
+
```
|
|
190
237
|
"""
|
|
191
238
|
|
|
192
239
|
AsyncToolCallWrapper = Callable[
|
|
@@ -530,6 +577,7 @@ class _ToolNode(RunnableCallable):
|
|
|
530
577
|
self._tools_by_name: dict[str, BaseTool] = {}
|
|
531
578
|
self._tool_to_state_args: dict[str, dict[str, str | None]] = {}
|
|
532
579
|
self._tool_to_store_arg: dict[str, str | None] = {}
|
|
580
|
+
self._tool_to_runtime_arg: dict[str, str | None] = {}
|
|
533
581
|
self._handle_tool_errors = handle_tool_errors
|
|
534
582
|
self._messages_key = messages_key
|
|
535
583
|
self._wrap_tool_call = wrap_tool_call
|
|
@@ -542,6 +590,7 @@ class _ToolNode(RunnableCallable):
|
|
|
542
590
|
self._tools_by_name[tool_.name] = tool_
|
|
543
591
|
self._tool_to_state_args[tool_.name] = _get_state_args(tool_)
|
|
544
592
|
self._tool_to_store_arg[tool_.name] = _get_store_arg(tool_)
|
|
593
|
+
self._tool_to_runtime_arg[tool_.name] = _get_runtime_arg(tool_)
|
|
545
594
|
|
|
546
595
|
@property
|
|
547
596
|
def tools_by_name(self) -> dict[str, BaseTool]:
|
|
@@ -552,26 +601,36 @@ class _ToolNode(RunnableCallable):
|
|
|
552
601
|
self,
|
|
553
602
|
input: list[AnyMessage] | dict[str, Any] | BaseModel,
|
|
554
603
|
config: RunnableConfig,
|
|
555
|
-
|
|
556
|
-
store: Optional[BaseStore], # noqa: UP045
|
|
604
|
+
runtime: Runtime,
|
|
557
605
|
) -> Any:
|
|
558
|
-
try:
|
|
559
|
-
runtime = get_runtime()
|
|
560
|
-
except RuntimeError:
|
|
561
|
-
# Running outside of LangGraph runtime context (e.g., unit tests)
|
|
562
|
-
runtime = None
|
|
563
|
-
|
|
564
606
|
tool_calls, input_type = self._parse_input(input)
|
|
565
|
-
tool_calls = [self._inject_tool_args(call, input, store) for call in tool_calls]
|
|
566
|
-
|
|
567
607
|
config_list = get_config_list(config, len(tool_calls))
|
|
608
|
+
|
|
609
|
+
# Construct ToolRuntime instances at the top level for each tool call
|
|
610
|
+
tool_runtimes = []
|
|
611
|
+
for call, cfg in zip(tool_calls, config_list, strict=False):
|
|
612
|
+
state = self._extract_state(input)
|
|
613
|
+
tool_runtime = ToolRuntime(
|
|
614
|
+
state=state,
|
|
615
|
+
tool_call_id=call["id"],
|
|
616
|
+
config=cfg,
|
|
617
|
+
context=runtime.context,
|
|
618
|
+
store=runtime.store,
|
|
619
|
+
stream_writer=runtime.stream_writer,
|
|
620
|
+
)
|
|
621
|
+
tool_runtimes.append(tool_runtime)
|
|
622
|
+
|
|
623
|
+
# Inject tool arguments (including runtime)
|
|
624
|
+
|
|
625
|
+
injected_tool_calls = []
|
|
568
626
|
input_types = [input_type] * len(tool_calls)
|
|
569
|
-
|
|
570
|
-
|
|
627
|
+
for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
|
|
628
|
+
injected_call = self._inject_tool_args(call, tool_runtime)
|
|
629
|
+
injected_tool_calls.append(injected_call)
|
|
571
630
|
with get_executor_for_config(config) as executor:
|
|
572
|
-
outputs =
|
|
573
|
-
|
|
574
|
-
|
|
631
|
+
outputs = list(
|
|
632
|
+
executor.map(self._run_one, injected_tool_calls, input_types, tool_runtimes)
|
|
633
|
+
)
|
|
575
634
|
|
|
576
635
|
return self._combine_tool_outputs(outputs, input_type)
|
|
577
636
|
|
|
@@ -579,20 +638,32 @@ class _ToolNode(RunnableCallable):
|
|
|
579
638
|
self,
|
|
580
639
|
input: list[AnyMessage] | dict[str, Any] | BaseModel,
|
|
581
640
|
config: RunnableConfig,
|
|
582
|
-
|
|
583
|
-
store: Optional[BaseStore], # noqa: UP045
|
|
641
|
+
runtime: Runtime,
|
|
584
642
|
) -> Any:
|
|
585
|
-
try:
|
|
586
|
-
runtime = get_runtime()
|
|
587
|
-
except RuntimeError:
|
|
588
|
-
# Running outside of LangGraph runtime context (e.g., unit tests)
|
|
589
|
-
runtime = None
|
|
590
|
-
|
|
591
643
|
tool_calls, input_type = self._parse_input(input)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
644
|
+
config_list = get_config_list(config, len(tool_calls))
|
|
645
|
+
|
|
646
|
+
# Construct ToolRuntime instances at the top level for each tool call
|
|
647
|
+
tool_runtimes = []
|
|
648
|
+
for call, cfg in zip(tool_calls, config_list, strict=False):
|
|
649
|
+
state = self._extract_state(input)
|
|
650
|
+
tool_runtime = ToolRuntime(
|
|
651
|
+
state=state,
|
|
652
|
+
tool_call_id=call["id"],
|
|
653
|
+
config=cfg,
|
|
654
|
+
context=runtime.context,
|
|
655
|
+
store=runtime.store,
|
|
656
|
+
stream_writer=runtime.stream_writer,
|
|
657
|
+
)
|
|
658
|
+
tool_runtimes.append(tool_runtime)
|
|
659
|
+
|
|
660
|
+
injected_tool_calls = []
|
|
661
|
+
coros = []
|
|
662
|
+
for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
|
|
663
|
+
injected_call = self._inject_tool_args(call, tool_runtime)
|
|
664
|
+
injected_tool_calls.append(injected_call)
|
|
665
|
+
coros.append(self._arun_one(injected_call, input_type, tool_runtime))
|
|
666
|
+
outputs = await asyncio.gather(*coros)
|
|
596
667
|
|
|
597
668
|
return self._combine_tool_outputs(outputs, input_type)
|
|
598
669
|
|
|
@@ -659,6 +730,15 @@ class _ToolNode(RunnableCallable):
|
|
|
659
730
|
"""
|
|
660
731
|
call = request.tool_call
|
|
661
732
|
tool = request.tool
|
|
733
|
+
|
|
734
|
+
# Validate tool exists when we actually need to execute it
|
|
735
|
+
if tool is None:
|
|
736
|
+
if invalid_tool_message := self._validate_tool_call(call):
|
|
737
|
+
return invalid_tool_message
|
|
738
|
+
# This should never happen if validation works correctly
|
|
739
|
+
msg = f"Tool {call['name']} is not registered with ToolNode"
|
|
740
|
+
raise TypeError(msg)
|
|
741
|
+
|
|
662
742
|
call_args = {**call, "type": "tool_call"}
|
|
663
743
|
|
|
664
744
|
try:
|
|
@@ -723,38 +803,32 @@ class _ToolNode(RunnableCallable):
|
|
|
723
803
|
self,
|
|
724
804
|
call: ToolCall,
|
|
725
805
|
input_type: Literal["list", "dict", "tool_calls"],
|
|
726
|
-
|
|
727
|
-
input: list[AnyMessage] | dict[str, Any] | BaseModel,
|
|
728
|
-
runtime: Any,
|
|
806
|
+
tool_runtime: ToolRuntime,
|
|
729
807
|
) -> ToolMessage | Command:
|
|
730
808
|
"""Execute single tool call with wrap_tool_call wrapper if configured.
|
|
731
809
|
|
|
732
810
|
Args:
|
|
733
811
|
call: Tool call dict.
|
|
734
812
|
input_type: Input format.
|
|
735
|
-
|
|
736
|
-
input: Agent state.
|
|
737
|
-
runtime: LangGraph runtime or None.
|
|
813
|
+
tool_runtime: Tool runtime.
|
|
738
814
|
|
|
739
815
|
Returns:
|
|
740
816
|
ToolMessage or Command.
|
|
741
817
|
"""
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
tool = self.tools_by_name[call["name"]]
|
|
746
|
-
|
|
747
|
-
# Extract state from ToolCallWithContext if present
|
|
748
|
-
state = self._extract_state(input)
|
|
818
|
+
# Validation is deferred to _execute_tool_sync to allow interceptors
|
|
819
|
+
# to short-circuit requests for unregistered tools
|
|
820
|
+
tool = self.tools_by_name.get(call["name"])
|
|
749
821
|
|
|
750
822
|
# Create the tool request with state and runtime
|
|
751
823
|
tool_request = ToolCallRequest(
|
|
752
824
|
tool_call=call,
|
|
753
825
|
tool=tool,
|
|
754
|
-
state=state,
|
|
755
|
-
runtime=
|
|
826
|
+
state=tool_runtime.state,
|
|
827
|
+
runtime=tool_runtime,
|
|
756
828
|
)
|
|
757
829
|
|
|
830
|
+
config = tool_runtime.config
|
|
831
|
+
|
|
758
832
|
if self._wrap_tool_call is None:
|
|
759
833
|
# No wrapper - execute directly
|
|
760
834
|
return self._execute_tool_sync(tool_request, input_type, config)
|
|
@@ -801,6 +875,15 @@ class _ToolNode(RunnableCallable):
|
|
|
801
875
|
"""
|
|
802
876
|
call = request.tool_call
|
|
803
877
|
tool = request.tool
|
|
878
|
+
|
|
879
|
+
# Validate tool exists when we actually need to execute it
|
|
880
|
+
if tool is None:
|
|
881
|
+
if invalid_tool_message := self._validate_tool_call(call):
|
|
882
|
+
return invalid_tool_message
|
|
883
|
+
# This should never happen if validation works correctly
|
|
884
|
+
msg = f"Tool {call['name']} is not registered with ToolNode"
|
|
885
|
+
raise TypeError(msg)
|
|
886
|
+
|
|
804
887
|
call_args = {**call, "type": "tool_call"}
|
|
805
888
|
|
|
806
889
|
try:
|
|
@@ -865,38 +948,32 @@ class _ToolNode(RunnableCallable):
|
|
|
865
948
|
self,
|
|
866
949
|
call: ToolCall,
|
|
867
950
|
input_type: Literal["list", "dict", "tool_calls"],
|
|
868
|
-
|
|
869
|
-
input: list[AnyMessage] | dict[str, Any] | BaseModel,
|
|
870
|
-
runtime: Any,
|
|
951
|
+
tool_runtime: ToolRuntime,
|
|
871
952
|
) -> ToolMessage | Command:
|
|
872
953
|
"""Execute single tool call asynchronously with awrap_tool_call wrapper if configured.
|
|
873
954
|
|
|
874
955
|
Args:
|
|
875
956
|
call: Tool call dict.
|
|
876
957
|
input_type: Input format.
|
|
877
|
-
|
|
878
|
-
input: Agent state.
|
|
879
|
-
runtime: LangGraph runtime or None.
|
|
958
|
+
tool_runtime: Tool runtime.
|
|
880
959
|
|
|
881
960
|
Returns:
|
|
882
961
|
ToolMessage or Command.
|
|
883
962
|
"""
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
tool = self.tools_by_name[call["name"]]
|
|
888
|
-
|
|
889
|
-
# Extract state from ToolCallWithContext if present
|
|
890
|
-
state = self._extract_state(input)
|
|
963
|
+
# Validation is deferred to _execute_tool_async to allow interceptors
|
|
964
|
+
# to short-circuit requests for unregistered tools
|
|
965
|
+
tool = self.tools_by_name.get(call["name"])
|
|
891
966
|
|
|
892
967
|
# Create the tool request with state and runtime
|
|
893
968
|
tool_request = ToolCallRequest(
|
|
894
969
|
tool_call=call,
|
|
895
970
|
tool=tool,
|
|
896
|
-
state=state,
|
|
897
|
-
runtime=
|
|
971
|
+
state=tool_runtime.state,
|
|
972
|
+
runtime=tool_runtime,
|
|
898
973
|
)
|
|
899
974
|
|
|
975
|
+
config = tool_runtime.config
|
|
976
|
+
|
|
900
977
|
if self._awrap_tool_call is None and self._wrap_tool_call is None:
|
|
901
978
|
# No wrapper - execute directly
|
|
902
979
|
return await self._execute_tool_async(tool_request, input_type, config)
|
|
@@ -999,15 +1076,16 @@ class _ToolNode(RunnableCallable):
|
|
|
999
1076
|
def _inject_state(
|
|
1000
1077
|
self,
|
|
1001
1078
|
tool_call: ToolCall,
|
|
1002
|
-
|
|
1079
|
+
state: list[AnyMessage] | dict[str, Any] | BaseModel,
|
|
1003
1080
|
) -> ToolCall:
|
|
1004
1081
|
state_args = self._tool_to_state_args[tool_call["name"]]
|
|
1005
|
-
|
|
1082
|
+
|
|
1083
|
+
if state_args and isinstance(state, list):
|
|
1006
1084
|
required_fields = list(state_args.values())
|
|
1007
1085
|
if (
|
|
1008
1086
|
len(required_fields) == 1 and required_fields[0] == self._messages_key
|
|
1009
1087
|
) or required_fields[0] is None:
|
|
1010
|
-
|
|
1088
|
+
state = {self._messages_key: state}
|
|
1011
1089
|
else:
|
|
1012
1090
|
err_msg = (
|
|
1013
1091
|
f"Invalid input to ToolNode. Tool {tool_call['name']} requires "
|
|
@@ -1018,12 +1096,6 @@ class _ToolNode(RunnableCallable):
|
|
|
1018
1096
|
err_msg += f" State should contain fields {required_fields_str}."
|
|
1019
1097
|
raise ValueError(err_msg)
|
|
1020
1098
|
|
|
1021
|
-
# Extract state from ToolCallWithContext if present
|
|
1022
|
-
if isinstance(input, dict) and input.get("__type") == "tool_call_with_context":
|
|
1023
|
-
state = input["state"]
|
|
1024
|
-
else:
|
|
1025
|
-
state = input
|
|
1026
|
-
|
|
1027
1099
|
if isinstance(state, dict):
|
|
1028
1100
|
tool_state_args = {
|
|
1029
1101
|
tool_arg: state[state_field] if state_field else state
|
|
@@ -1059,19 +1131,38 @@ class _ToolNode(RunnableCallable):
|
|
|
1059
1131
|
}
|
|
1060
1132
|
return tool_call
|
|
1061
1133
|
|
|
1134
|
+
def _inject_runtime(self, tool_call: ToolCall, tool_runtime: ToolRuntime) -> ToolCall:
|
|
1135
|
+
"""Inject ToolRuntime into tool call arguments.
|
|
1136
|
+
|
|
1137
|
+
Args:
|
|
1138
|
+
tool_call: The tool call to inject runtime into.
|
|
1139
|
+
tool_runtime: The ToolRuntime instance to inject.
|
|
1140
|
+
|
|
1141
|
+
Returns:
|
|
1142
|
+
The tool call with runtime injected if needed.
|
|
1143
|
+
"""
|
|
1144
|
+
runtime_arg = self._tool_to_runtime_arg.get(tool_call["name"])
|
|
1145
|
+
if not runtime_arg:
|
|
1146
|
+
return tool_call
|
|
1147
|
+
|
|
1148
|
+
tool_call["args"] = {
|
|
1149
|
+
**tool_call["args"],
|
|
1150
|
+
runtime_arg: tool_runtime,
|
|
1151
|
+
}
|
|
1152
|
+
return tool_call
|
|
1153
|
+
|
|
1062
1154
|
def _inject_tool_args(
|
|
1063
1155
|
self,
|
|
1064
1156
|
tool_call: ToolCall,
|
|
1065
|
-
|
|
1066
|
-
store: BaseStore | None,
|
|
1157
|
+
tool_runtime: ToolRuntime,
|
|
1067
1158
|
) -> ToolCall:
|
|
1068
|
-
"""Inject graph state and
|
|
1159
|
+
"""Inject graph state, store, and runtime into tool call arguments.
|
|
1069
1160
|
|
|
1070
1161
|
This is an internal method that enables tools to access graph context that
|
|
1071
1162
|
should not be controlled by the model. Tools can declare dependencies on graph
|
|
1072
|
-
state
|
|
1073
|
-
This method automatically identifies these
|
|
1074
|
-
appropriate values.
|
|
1163
|
+
state, persistent storage, or runtime context using InjectedState, InjectedStore,
|
|
1164
|
+
and ToolRuntime annotations. This method automatically identifies these
|
|
1165
|
+
dependencies and injects the appropriate values.
|
|
1075
1166
|
|
|
1076
1167
|
The injection process preserves the original tool call structure while adding
|
|
1077
1168
|
the necessary context arguments. This allows tools to be both model-callable
|
|
@@ -1080,10 +1171,8 @@ class _ToolNode(RunnableCallable):
|
|
|
1080
1171
|
Args:
|
|
1081
1172
|
tool_call: The tool call dictionary to augment with injected arguments.
|
|
1082
1173
|
Must contain 'name', 'args', 'id', and 'type' fields.
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
store: The persistent store instance to inject into tools requiring storage.
|
|
1086
|
-
Will be None if no store is configured for the graph.
|
|
1174
|
+
tool_runtime: The ToolRuntime instance containing all runtime context
|
|
1175
|
+
(state, config, store, context, stream_writer) to inject into tools.
|
|
1087
1176
|
|
|
1088
1177
|
Returns:
|
|
1089
1178
|
A new ToolCall dictionary with the same structure as the input but with
|
|
@@ -1101,8 +1190,9 @@ class _ToolNode(RunnableCallable):
|
|
|
1101
1190
|
return tool_call
|
|
1102
1191
|
|
|
1103
1192
|
tool_call_copy: ToolCall = copy(tool_call)
|
|
1104
|
-
tool_call_with_state = self._inject_state(tool_call_copy,
|
|
1105
|
-
|
|
1193
|
+
tool_call_with_state = self._inject_state(tool_call_copy, tool_runtime.state)
|
|
1194
|
+
tool_call_with_store = self._inject_store(tool_call_with_state, tool_runtime.store)
|
|
1195
|
+
return self._inject_runtime(tool_call_with_store, tool_runtime)
|
|
1106
1196
|
|
|
1107
1197
|
def _validate_tool_command(
|
|
1108
1198
|
self,
|
|
@@ -1258,11 +1348,71 @@ def tools_condition(
|
|
|
1258
1348
|
return "__end__"
|
|
1259
1349
|
|
|
1260
1350
|
|
|
1351
|
+
@dataclass
|
|
1352
|
+
class ToolRuntime(InjectedToolArg, Generic[ContextT, StateT]):
|
|
1353
|
+
"""Runtime context automatically injected into tools.
|
|
1354
|
+
|
|
1355
|
+
When a tool function has a parameter named 'tool_runtime' with type hint
|
|
1356
|
+
'ToolRuntime', the tool execution system will automatically inject
|
|
1357
|
+
an instance containing:
|
|
1358
|
+
|
|
1359
|
+
- state: The current graph state
|
|
1360
|
+
- tool_call_id: The ID of the current tool call
|
|
1361
|
+
- config: RunnableConfig for the current execution
|
|
1362
|
+
- context: Runtime context (from langgraph Runtime)
|
|
1363
|
+
- store: BaseStore instance for persistent storage (from langgraph Runtime)
|
|
1364
|
+
- stream_writer: StreamWriter for streaming output (from langgraph Runtime)
|
|
1365
|
+
|
|
1366
|
+
No `Annotated` wrapper is needed - just use `runtime: ToolRuntime`
|
|
1367
|
+
as a parameter.
|
|
1368
|
+
|
|
1369
|
+
Example:
|
|
1370
|
+
```python
|
|
1371
|
+
from langchain_core.tools import tool
|
|
1372
|
+
from langchain.tools import ToolRuntime
|
|
1373
|
+
|
|
1374
|
+
@tool
|
|
1375
|
+
def my_tool(x: int, runtime: ToolRuntime) -> str:
|
|
1376
|
+
\"\"\"Tool that accesses runtime context.\"\"\"
|
|
1377
|
+
# Access state
|
|
1378
|
+
messages = tool_runtime.state["messages"]
|
|
1379
|
+
|
|
1380
|
+
# Access tool_call_id
|
|
1381
|
+
print(f"Tool call ID: {tool_runtime.tool_call_id}")
|
|
1382
|
+
|
|
1383
|
+
# Access config
|
|
1384
|
+
print(f"Run ID: {tool_runtime.config.get('run_id')}")
|
|
1385
|
+
|
|
1386
|
+
# Access runtime context
|
|
1387
|
+
user_id = tool_runtime.context.get("user_id")
|
|
1388
|
+
|
|
1389
|
+
# Access store
|
|
1390
|
+
tool_runtime.store.put(("metrics",), "count", 1)
|
|
1391
|
+
|
|
1392
|
+
# Stream output
|
|
1393
|
+
tool_runtime.stream_writer.write("Processing...")
|
|
1394
|
+
|
|
1395
|
+
return f"Processed {x}"
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
Note:
|
|
1399
|
+
This is a marker class used for type checking and detection.
|
|
1400
|
+
The actual runtime object will be constructed during tool execution.
|
|
1401
|
+
"""
|
|
1402
|
+
|
|
1403
|
+
state: StateT
|
|
1404
|
+
context: ContextT
|
|
1405
|
+
config: RunnableConfig
|
|
1406
|
+
stream_writer: StreamWriter
|
|
1407
|
+
tool_call_id: str | None
|
|
1408
|
+
store: BaseStore | None
|
|
1409
|
+
|
|
1410
|
+
|
|
1261
1411
|
class InjectedState(InjectedToolArg):
|
|
1262
1412
|
"""Annotation for injecting graph state into tool arguments.
|
|
1263
1413
|
|
|
1264
1414
|
This annotation enables tools to access graph state without exposing state
|
|
1265
|
-
management details to the language model. Tools annotated with InjectedState
|
|
1415
|
+
management details to the language model. Tools annotated with `InjectedState`
|
|
1266
1416
|
receive state data automatically during execution while remaining invisible
|
|
1267
1417
|
to the model's tool-calling interface.
|
|
1268
1418
|
|
|
@@ -1320,9 +1470,9 @@ class InjectedState(InjectedToolArg):
|
|
|
1320
1470
|
```
|
|
1321
1471
|
|
|
1322
1472
|
Note:
|
|
1323
|
-
- InjectedState arguments are automatically excluded from tool schemas
|
|
1473
|
+
- `InjectedState` arguments are automatically excluded from tool schemas
|
|
1324
1474
|
presented to language models
|
|
1325
|
-
- ToolNode handles the injection process during execution
|
|
1475
|
+
- `ToolNode` handles the injection process during execution
|
|
1326
1476
|
- Tools can mix regular arguments (controlled by the model) with injected
|
|
1327
1477
|
arguments (controlled by the system)
|
|
1328
1478
|
- State injection occurs after the model generates tool calls but before
|
|
@@ -1330,7 +1480,7 @@ class InjectedState(InjectedToolArg):
|
|
|
1330
1480
|
"""
|
|
1331
1481
|
|
|
1332
1482
|
def __init__(self, field: str | None = None) -> None:
|
|
1333
|
-
"""Initialize the InjectedState annotation."""
|
|
1483
|
+
"""Initialize the `InjectedState` annotation."""
|
|
1334
1484
|
self.field = field
|
|
1335
1485
|
|
|
1336
1486
|
|
|
@@ -1375,7 +1525,7 @@ class InjectedStore(InjectedToolArg):
|
|
|
1375
1525
|
return result.value if result else "Not found"
|
|
1376
1526
|
```
|
|
1377
1527
|
|
|
1378
|
-
Usage with ToolNode and graph compilation:
|
|
1528
|
+
Usage with `ToolNode` and graph compilation:
|
|
1379
1529
|
|
|
1380
1530
|
```python
|
|
1381
1531
|
from langgraph.graph import StateGraph
|
|
@@ -1400,16 +1550,19 @@ class InjectedStore(InjectedToolArg):
|
|
|
1400
1550
|
```
|
|
1401
1551
|
|
|
1402
1552
|
Note:
|
|
1403
|
-
- InjectedStore arguments are automatically excluded from tool schemas
|
|
1553
|
+
- `InjectedStore` arguments are automatically excluded from tool schemas
|
|
1404
1554
|
presented to language models
|
|
1405
|
-
- The store instance is automatically injected by ToolNode during execution
|
|
1555
|
+
- The store instance is automatically injected by `ToolNode` during execution
|
|
1406
1556
|
- Tools can access namespaced storage using the store's get/put methods
|
|
1407
1557
|
- Store injection requires the graph to be compiled with a store instance
|
|
1408
1558
|
- Multiple tools can share the same store instance for data consistency
|
|
1409
1559
|
"""
|
|
1410
1560
|
|
|
1411
1561
|
|
|
1412
|
-
def _is_injection(
|
|
1562
|
+
def _is_injection(
|
|
1563
|
+
type_arg: Any,
|
|
1564
|
+
injection_type: type[InjectedState | InjectedStore | ToolRuntime],
|
|
1565
|
+
) -> bool:
|
|
1413
1566
|
"""Check if a type argument represents an injection annotation.
|
|
1414
1567
|
|
|
1415
1568
|
This utility function determines whether a type annotation indicates that
|
|
@@ -1503,3 +1656,44 @@ def _get_store_arg(tool: BaseTool) -> str | None:
|
|
|
1503
1656
|
return name
|
|
1504
1657
|
|
|
1505
1658
|
return None
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def _get_runtime_arg(tool: BaseTool) -> str | None:
|
|
1662
|
+
"""Extract runtime injection argument from tool annotations.
|
|
1663
|
+
|
|
1664
|
+
This function analyzes a tool's input schema to identify the argument that
|
|
1665
|
+
should be injected with the ToolRuntime instance. Only one runtime argument
|
|
1666
|
+
is supported per tool.
|
|
1667
|
+
|
|
1668
|
+
Args:
|
|
1669
|
+
tool: The tool to analyze for runtime injection requirements.
|
|
1670
|
+
|
|
1671
|
+
Returns:
|
|
1672
|
+
The name of the argument that should receive the runtime injection, or None
|
|
1673
|
+
if no runtime injection is required.
|
|
1674
|
+
|
|
1675
|
+
Raises:
|
|
1676
|
+
ValueError: If a tool argument has multiple ToolRuntime annotations.
|
|
1677
|
+
"""
|
|
1678
|
+
full_schema = tool.get_input_schema()
|
|
1679
|
+
for name, type_ in get_all_basemodel_annotations(full_schema).items():
|
|
1680
|
+
# Check if the parameter name is "runtime" (regardless of type)
|
|
1681
|
+
if name == "runtime":
|
|
1682
|
+
return name
|
|
1683
|
+
# Check if the type itself is ToolRuntime (direct usage)
|
|
1684
|
+
if _is_injection(type_, ToolRuntime):
|
|
1685
|
+
return name
|
|
1686
|
+
# Check if ToolRuntime is in Annotated args
|
|
1687
|
+
injections = [
|
|
1688
|
+
type_arg for type_arg in get_args(type_) if _is_injection(type_arg, ToolRuntime)
|
|
1689
|
+
]
|
|
1690
|
+
if len(injections) > 1:
|
|
1691
|
+
msg = (
|
|
1692
|
+
"A tool argument should not be annotated with ToolRuntime more than "
|
|
1693
|
+
f"once. Received arg {name} with annotations {injections}."
|
|
1694
|
+
)
|
|
1695
|
+
raise ValueError(msg)
|
|
1696
|
+
if len(injections) == 1:
|
|
1697
|
+
return name
|
|
1698
|
+
|
|
1699
|
+
return None
|