langchain 1.0.1__py3-none-any.whl → 1.0.2__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/tools/tool_node.py +103 -19
- {langchain-1.0.1.dist-info → langchain-1.0.2.dist-info}/METADATA +1 -1
- {langchain-1.0.1.dist-info → langchain-1.0.2.dist-info}/RECORD +5 -5
- {langchain-1.0.1.dist-info → langchain-1.0.2.dist-info}/WHEEL +0 -0
- {langchain-1.0.1.dist-info → langchain-1.0.2.dist-info}/licenses/LICENSE +0 -0
langchain/tools/tool_node.py
CHANGED
|
@@ -89,6 +89,7 @@ if TYPE_CHECKING:
|
|
|
89
89
|
from collections.abc import Sequence
|
|
90
90
|
|
|
91
91
|
from langgraph.runtime import Runtime
|
|
92
|
+
from pydantic_core import ErrorDetails
|
|
92
93
|
|
|
93
94
|
# right now we use a dict as the default, can change this to AgentState, but depends
|
|
94
95
|
# on if this lives in LangChain or LangGraph... ideally would have some typed
|
|
@@ -303,7 +304,11 @@ class ToolInvocationError(ToolException):
|
|
|
303
304
|
"""
|
|
304
305
|
|
|
305
306
|
def __init__(
|
|
306
|
-
self,
|
|
307
|
+
self,
|
|
308
|
+
tool_name: str,
|
|
309
|
+
source: ValidationError,
|
|
310
|
+
tool_kwargs: dict[str, Any],
|
|
311
|
+
filtered_errors: list[ErrorDetails] | None = None,
|
|
307
312
|
) -> None:
|
|
308
313
|
"""Initialize the ToolInvocationError.
|
|
309
314
|
|
|
@@ -311,13 +316,28 @@ class ToolInvocationError(ToolException):
|
|
|
311
316
|
tool_name: The name of the tool that failed.
|
|
312
317
|
source: The exception that occurred.
|
|
313
318
|
tool_kwargs: The keyword arguments that were passed to the tool.
|
|
319
|
+
filtered_errors: Optional list of filtered validation errors excluding
|
|
320
|
+
injected arguments.
|
|
314
321
|
"""
|
|
322
|
+
# Format error display based on filtered errors if provided
|
|
323
|
+
if filtered_errors is not None:
|
|
324
|
+
# Manually format the filtered errors without URLs or fancy formatting
|
|
325
|
+
error_str_parts = []
|
|
326
|
+
for error in filtered_errors:
|
|
327
|
+
loc_str = ".".join(str(loc) for loc in error.get("loc", ()))
|
|
328
|
+
msg = error.get("msg", "Unknown error")
|
|
329
|
+
error_str_parts.append(f"{loc_str}: {msg}")
|
|
330
|
+
error_display_str = "\n".join(error_str_parts)
|
|
331
|
+
else:
|
|
332
|
+
error_display_str = str(source)
|
|
333
|
+
|
|
315
334
|
self.message = TOOL_INVOCATION_ERROR_TEMPLATE.format(
|
|
316
|
-
tool_name=tool_name, tool_kwargs=tool_kwargs, error=
|
|
335
|
+
tool_name=tool_name, tool_kwargs=tool_kwargs, error=error_display_str
|
|
317
336
|
)
|
|
318
337
|
self.tool_name = tool_name
|
|
319
338
|
self.tool_kwargs = tool_kwargs
|
|
320
339
|
self.source = source
|
|
340
|
+
self.filtered_errors = filtered_errors
|
|
321
341
|
super().__init__(self.message)
|
|
322
342
|
|
|
323
343
|
|
|
@@ -442,6 +462,59 @@ def _infer_handled_types(handler: Callable[..., str]) -> tuple[type[Exception],
|
|
|
442
462
|
return (Exception,)
|
|
443
463
|
|
|
444
464
|
|
|
465
|
+
def _filter_validation_errors(
|
|
466
|
+
validation_error: ValidationError,
|
|
467
|
+
tool_to_state_args: dict[str, str | None],
|
|
468
|
+
tool_to_store_arg: str | None,
|
|
469
|
+
tool_to_runtime_arg: str | None,
|
|
470
|
+
) -> list[ErrorDetails]:
|
|
471
|
+
"""Filter validation errors to only include LLM-controlled arguments.
|
|
472
|
+
|
|
473
|
+
When a tool invocation fails validation, only errors for arguments that the LLM
|
|
474
|
+
controls should be included in error messages. This ensures the LLM receives
|
|
475
|
+
focused, actionable feedback about parameters it can actually fix. System-injected
|
|
476
|
+
arguments (state, store, runtime) are filtered out since the LLM has no control
|
|
477
|
+
over them.
|
|
478
|
+
|
|
479
|
+
This function also removes injected argument values from the `input` field in error
|
|
480
|
+
details, ensuring that only LLM-provided arguments appear in error messages.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
validation_error: The Pydantic ValidationError raised during tool invocation.
|
|
484
|
+
tool_to_state_args: Mapping of state argument names to state field names.
|
|
485
|
+
tool_to_store_arg: Name of the store argument, if any.
|
|
486
|
+
tool_to_runtime_arg: Name of the runtime argument, if any.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
List of ErrorDetails containing only errors for LLM-controlled arguments,
|
|
490
|
+
with system-injected argument values removed from the input field.
|
|
491
|
+
"""
|
|
492
|
+
injected_args = set(tool_to_state_args.keys())
|
|
493
|
+
if tool_to_store_arg:
|
|
494
|
+
injected_args.add(tool_to_store_arg)
|
|
495
|
+
if tool_to_runtime_arg:
|
|
496
|
+
injected_args.add(tool_to_runtime_arg)
|
|
497
|
+
|
|
498
|
+
filtered_errors: list[ErrorDetails] = []
|
|
499
|
+
for error in validation_error.errors():
|
|
500
|
+
# Check if error location contains any injected argument
|
|
501
|
+
# error['loc'] is a tuple like ('field_name',) or ('field_name', 'nested_field')
|
|
502
|
+
if error["loc"] and error["loc"][0] not in injected_args:
|
|
503
|
+
# Create a copy of the error dict to avoid mutating the original
|
|
504
|
+
error_copy: dict[str, Any] = {**error}
|
|
505
|
+
|
|
506
|
+
# Remove injected arguments from input_value if it's a dict
|
|
507
|
+
if isinstance(error_copy.get("input"), dict):
|
|
508
|
+
input_dict = error_copy["input"]
|
|
509
|
+
input_copy = {k: v for k, v in input_dict.items() if k not in injected_args}
|
|
510
|
+
error_copy["input"] = input_copy
|
|
511
|
+
|
|
512
|
+
# Cast is safe because ErrorDetails is a TypedDict compatible with this structure
|
|
513
|
+
filtered_errors.append(error_copy) # type: ignore[arg-type]
|
|
514
|
+
|
|
515
|
+
return filtered_errors
|
|
516
|
+
|
|
517
|
+
|
|
445
518
|
class _ToolNode(RunnableCallable):
|
|
446
519
|
"""A node for executing tools in LangGraph workflows.
|
|
447
520
|
|
|
@@ -623,17 +696,10 @@ class _ToolNode(RunnableCallable):
|
|
|
623
696
|
)
|
|
624
697
|
tool_runtimes.append(tool_runtime)
|
|
625
698
|
|
|
626
|
-
#
|
|
627
|
-
|
|
628
|
-
injected_tool_calls = []
|
|
699
|
+
# Pass original tool calls without injection
|
|
629
700
|
input_types = [input_type] * len(tool_calls)
|
|
630
|
-
for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
|
|
631
|
-
injected_call = self._inject_tool_args(call, tool_runtime) # type: ignore[arg-type]
|
|
632
|
-
injected_tool_calls.append(injected_call)
|
|
633
701
|
with get_executor_for_config(config) as executor:
|
|
634
|
-
outputs = list(
|
|
635
|
-
executor.map(self._run_one, injected_tool_calls, input_types, tool_runtimes)
|
|
636
|
-
)
|
|
702
|
+
outputs = list(executor.map(self._run_one, tool_calls, input_types, tool_runtimes))
|
|
637
703
|
|
|
638
704
|
return self._combine_tool_outputs(outputs, input_type)
|
|
639
705
|
|
|
@@ -660,12 +726,10 @@ class _ToolNode(RunnableCallable):
|
|
|
660
726
|
)
|
|
661
727
|
tool_runtimes.append(tool_runtime)
|
|
662
728
|
|
|
663
|
-
|
|
729
|
+
# Pass original tool calls without injection
|
|
664
730
|
coros = []
|
|
665
731
|
for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
|
|
666
|
-
|
|
667
|
-
injected_tool_calls.append(injected_call)
|
|
668
|
-
coros.append(self._arun_one(injected_call, input_type, tool_runtime)) # type: ignore[arg-type]
|
|
732
|
+
coros.append(self._arun_one(call, input_type, tool_runtime)) # type: ignore[arg-type]
|
|
669
733
|
outputs = await asyncio.gather(*coros)
|
|
670
734
|
|
|
671
735
|
return self._combine_tool_outputs(outputs, input_type)
|
|
@@ -742,13 +806,23 @@ class _ToolNode(RunnableCallable):
|
|
|
742
806
|
msg = f"Tool {call['name']} is not registered with ToolNode"
|
|
743
807
|
raise TypeError(msg)
|
|
744
808
|
|
|
745
|
-
|
|
809
|
+
# Inject state, store, and runtime right before invocation
|
|
810
|
+
injected_call = self._inject_tool_args(call, request.runtime)
|
|
811
|
+
call_args = {**injected_call, "type": "tool_call"}
|
|
746
812
|
|
|
747
813
|
try:
|
|
748
814
|
try:
|
|
749
815
|
response = tool.invoke(call_args, config)
|
|
750
816
|
except ValidationError as exc:
|
|
751
|
-
|
|
817
|
+
# Filter out errors for injected arguments
|
|
818
|
+
filtered_errors = _filter_validation_errors(
|
|
819
|
+
exc,
|
|
820
|
+
self._tool_to_state_args.get(call["name"], {}),
|
|
821
|
+
self._tool_to_store_arg.get(call["name"]),
|
|
822
|
+
self._tool_to_runtime_arg.get(call["name"]),
|
|
823
|
+
)
|
|
824
|
+
# Use original call["args"] without injected values for error reporting
|
|
825
|
+
raise ToolInvocationError(call["name"], exc, call["args"], filtered_errors) from exc
|
|
752
826
|
|
|
753
827
|
# GraphInterrupt is a special exception that will always be raised.
|
|
754
828
|
# It can be triggered in the following scenarios,
|
|
@@ -887,13 +961,23 @@ class _ToolNode(RunnableCallable):
|
|
|
887
961
|
msg = f"Tool {call['name']} is not registered with ToolNode"
|
|
888
962
|
raise TypeError(msg)
|
|
889
963
|
|
|
890
|
-
|
|
964
|
+
# Inject state, store, and runtime right before invocation
|
|
965
|
+
injected_call = self._inject_tool_args(call, request.runtime)
|
|
966
|
+
call_args = {**injected_call, "type": "tool_call"}
|
|
891
967
|
|
|
892
968
|
try:
|
|
893
969
|
try:
|
|
894
970
|
response = await tool.ainvoke(call_args, config)
|
|
895
971
|
except ValidationError as exc:
|
|
896
|
-
|
|
972
|
+
# Filter out errors for injected arguments
|
|
973
|
+
filtered_errors = _filter_validation_errors(
|
|
974
|
+
exc,
|
|
975
|
+
self._tool_to_state_args.get(call["name"], {}),
|
|
976
|
+
self._tool_to_store_arg.get(call["name"]),
|
|
977
|
+
self._tool_to_runtime_arg.get(call["name"]),
|
|
978
|
+
)
|
|
979
|
+
# Use original call["args"] without injected values for error reporting
|
|
980
|
+
raise ToolInvocationError(call["name"], exc, call["args"], filtered_errors) from exc
|
|
897
981
|
|
|
898
982
|
# GraphInterrupt is a special exception that will always be raised.
|
|
899
983
|
# It can be triggered in the following scenarios,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Building applications with LLMs through composability
|
|
5
5
|
Project-URL: Homepage, https://docs.langchain.com/
|
|
6
6
|
Project-URL: Documentation, https://reference.langchain.com/python/langchain/langchain/
|
|
@@ -27,8 +27,8 @@ langchain/embeddings/base.py,sha256=V9YgYiRAJs5U5o8BQUsmfV8XtRQrRWV3rENXHLKiECg,
|
|
|
27
27
|
langchain/messages/__init__.py,sha256=p7NlF1yf8MkMgJzJ2wggXGkkA_okz1f-g63KoflL6PA,1710
|
|
28
28
|
langchain/rate_limiters/__init__.py,sha256=5490xUNhet37N2nX6kbJlDgf8u1DX-C1Cs_r7etXn8A,351
|
|
29
29
|
langchain/tools/__init__.py,sha256=hMzbaGcfHhNYfJx20uV57uMd9a-gNLbmopG4gDReeEc,628
|
|
30
|
-
langchain/tools/tool_node.py,sha256=
|
|
31
|
-
langchain-1.0.
|
|
32
|
-
langchain-1.0.
|
|
33
|
-
langchain-1.0.
|
|
34
|
-
langchain-1.0.
|
|
30
|
+
langchain/tools/tool_node.py,sha256=3wHSg6TQk43Zv3xrkuLwYZn5Hv5w3oZOv0vkmI_40sQ,69489
|
|
31
|
+
langchain-1.0.2.dist-info/METADATA,sha256=XNAflqRgEm9FqOvFckJKFFhjmP4ouNOJ8HdPXJrKgbc,4709
|
|
32
|
+
langchain-1.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
33
|
+
langchain-1.0.2.dist-info/licenses/LICENSE,sha256=TsZ-TKbmch26hJssqCJhWXyGph7iFLvyFBYAa3stBHg,1067
|
|
34
|
+
langchain-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|