langchain 1.0.1__tar.gz → 1.0.2__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.
- {langchain-1.0.1 → langchain-1.0.2}/PKG-INFO +1 -1
- {langchain-1.0.1 → langchain-1.0.2}/langchain/tools/tool_node.py +103 -19
- {langchain-1.0.1 → langchain-1.0.2}/pyproject.toml +1 -1
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_tool_node.py +170 -4
- langchain-1.0.2/tests/unit_tests/agents/test_tool_node_validation_error_filtering.py +678 -0
- {langchain-1.0.1 → langchain-1.0.2}/uv.lock +2 -3
- {langchain-1.0.1 → langchain-1.0.2}/.gitignore +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/LICENSE +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/Makefile +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/README.md +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/extended_testing_deps.txt +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/factory.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/_execution.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/_redaction.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/context_editing.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/file_search.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/human_in_the_loop.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/model_call_limit.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/model_fallback.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/pii.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/shell_tool.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/summarization.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/todo.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/tool_call_limit.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/tool_emulator.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/tool_retry.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/tool_selection.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/types.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/structured_output.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/chat_models/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/chat_models/base.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/embeddings/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/embeddings/base.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/messages/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/py.typed +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/rate_limiters/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/langchain/tools/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/scripts/check_imports.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/agents/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/agents/middleware/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/agents/middleware/test_shell_tool_integration.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/agents/test_response_format.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/cache/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/cache/fake_embeddings.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/chat_models/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/chat_models/test_base.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/conftest.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/embeddings/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/embeddings/test_base.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/test_compile.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/__snapshots__/test_middleware_agent.ambr +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/__snapshots__/test_middleware_decorators.ambr +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/__snapshots__/test_return_direct_graph.ambr +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/any_str.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/compose-postgres.yml +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/compose-redis.yml +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/conftest.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/conftest_checkpointer.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/conftest_store.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/memory_assert.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/messages.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_before_after_agent.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_file_search.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_llm_tool_selection.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_override_methods.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_shell_execution_policies.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_shell_tool.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_tool_emulator.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_tool_retry.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_wrap_model_call_decorator.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_wrap_model_call_middleware.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_wrap_tool_call_decorator.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/model.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/specifications/responses.json +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/specifications/return_direct.json +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_context_editing_middleware.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_handler_composition.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_injected_runtime_create_agent.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_middleware_agent.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_middleware_decorators.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_middleware_tools.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_model_fallback_middleware.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_on_tool_call_middleware.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_pii_middleware.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_react_agent.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_response_format.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_responses.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_responses_spec.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_return_direct_graph.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_return_direct_spec.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_state_schema.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_sync_async_tool_wrapper_composition.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_todo_middleware.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_tool_call_limit.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_tool_node_interceptor_unregistered.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/utils.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/chat_models/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/chat_models/test_chat_models.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/conftest.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/embeddings/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/embeddings/test_base.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/embeddings/test_imports.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/stubs.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/test_dependencies.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/test_imports.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/test_pytest_config.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/tools/__init__.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/tools/test_imports.py +0 -0
- {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/tools/test_on_tool_call.py +0 -0
|
@@ -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/
|
|
@@ -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,
|
|
@@ -10,6 +10,8 @@ from typing import (
|
|
|
10
10
|
TypeVar,
|
|
11
11
|
)
|
|
12
12
|
from unittest.mock import Mock
|
|
13
|
+
from langchain.agents import create_agent
|
|
14
|
+
from langchain.agents.middleware.types import AgentState
|
|
13
15
|
|
|
14
16
|
import pytest
|
|
15
17
|
from langchain_core.messages import (
|
|
@@ -302,6 +304,172 @@ def test_tool_node_error_handling_default_exception() -> None:
|
|
|
302
304
|
)
|
|
303
305
|
|
|
304
306
|
|
|
307
|
+
@pytest.mark.skipif(
|
|
308
|
+
sys.version_info >= (3, 14), reason="Pydantic model rebuild issue in Python 3.14"
|
|
309
|
+
)
|
|
310
|
+
def test_tool_invocation_error_excludes_injected_state() -> None:
|
|
311
|
+
"""Test that tool invocation errors only include LLM-controllable arguments.
|
|
312
|
+
|
|
313
|
+
When a tool has InjectedState parameters and the LLM makes an incorrect
|
|
314
|
+
invocation (e.g., missing required arguments), the error message should only
|
|
315
|
+
contain the arguments from the tool call that the LLM controls. This ensures
|
|
316
|
+
the LLM receives relevant context to correct its mistakes, without being
|
|
317
|
+
distracted by system-injected parameters it has no control over.
|
|
318
|
+
|
|
319
|
+
This test uses create_agent to ensure the behavior works in a full agent context.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
# Define a custom state schema with injected data
|
|
323
|
+
class TestState(AgentState):
|
|
324
|
+
secret_data: str # Example of state data not controlled by LLM
|
|
325
|
+
|
|
326
|
+
@dec_tool
|
|
327
|
+
def tool_with_injected_state(
|
|
328
|
+
some_val: int,
|
|
329
|
+
state: Annotated[TestState, InjectedState],
|
|
330
|
+
) -> str:
|
|
331
|
+
"""Tool that uses injected state."""
|
|
332
|
+
return f"some_val: {some_val}"
|
|
333
|
+
|
|
334
|
+
# Create a fake model that makes an incorrect tool call (missing 'some_val')
|
|
335
|
+
# Then returns no tool calls on the second iteration to end the loop
|
|
336
|
+
model = FakeToolCallingModel(
|
|
337
|
+
tool_calls=[
|
|
338
|
+
[
|
|
339
|
+
{
|
|
340
|
+
"name": "tool_with_injected_state",
|
|
341
|
+
"args": {"wrong_arg": "value"}, # Missing required 'some_val'
|
|
342
|
+
"id": "call_1",
|
|
343
|
+
}
|
|
344
|
+
],
|
|
345
|
+
[], # No tool calls on second iteration to end the loop
|
|
346
|
+
]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Create an agent with the tool and custom state schema
|
|
350
|
+
agent = create_agent(
|
|
351
|
+
model=model,
|
|
352
|
+
tools=[tool_with_injected_state],
|
|
353
|
+
state_schema=TestState,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Invoke the agent with injected state data
|
|
357
|
+
result = agent.invoke(
|
|
358
|
+
{
|
|
359
|
+
"messages": [HumanMessage("Test message")],
|
|
360
|
+
"secret_data": "sensitive_secret_123",
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Find the tool error message
|
|
365
|
+
tool_messages = [m for m in result["messages"] if m.type == "tool"]
|
|
366
|
+
assert len(tool_messages) == 1
|
|
367
|
+
tool_message = tool_messages[0]
|
|
368
|
+
assert tool_message.status == "error"
|
|
369
|
+
|
|
370
|
+
# The error message should contain only the LLM-provided args (wrong_arg)
|
|
371
|
+
# and NOT the system-injected state (secret_data)
|
|
372
|
+
assert "{'wrong_arg': 'value'}" in tool_message.content
|
|
373
|
+
assert "secret_data" not in tool_message.content
|
|
374
|
+
assert "sensitive_secret_123" not in tool_message.content
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@pytest.mark.skipif(
|
|
378
|
+
sys.version_info >= (3, 14), reason="Pydantic model rebuild issue in Python 3.14"
|
|
379
|
+
)
|
|
380
|
+
async def test_tool_invocation_error_excludes_injected_state_async() -> None:
|
|
381
|
+
"""Test that async tool invocation errors only include LLM-controllable arguments.
|
|
382
|
+
|
|
383
|
+
This test verifies that the async execution path (_execute_tool_async and _arun_one)
|
|
384
|
+
properly filters validation errors to exclude system-injected arguments, ensuring
|
|
385
|
+
the LLM receives only relevant context for correction.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
# Define a custom state schema
|
|
389
|
+
class TestState(AgentState):
|
|
390
|
+
internal_data: str
|
|
391
|
+
|
|
392
|
+
@dec_tool
|
|
393
|
+
async def async_tool_with_injected_state(
|
|
394
|
+
query: str,
|
|
395
|
+
max_results: int,
|
|
396
|
+
state: Annotated[TestState, InjectedState],
|
|
397
|
+
) -> str:
|
|
398
|
+
"""Async tool that uses injected state."""
|
|
399
|
+
return f"query: {query}, max_results: {max_results}"
|
|
400
|
+
|
|
401
|
+
# Create a fake model that makes an incorrect tool call
|
|
402
|
+
# - query has wrong type (int instead of str)
|
|
403
|
+
# - max_results is missing
|
|
404
|
+
model = FakeToolCallingModel(
|
|
405
|
+
tool_calls=[
|
|
406
|
+
[
|
|
407
|
+
{
|
|
408
|
+
"name": "async_tool_with_injected_state",
|
|
409
|
+
"args": {"query": 999}, # Wrong type, missing max_results
|
|
410
|
+
"id": "call_async_1",
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
[], # End the loop
|
|
414
|
+
]
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Create an agent with the async tool
|
|
418
|
+
agent = create_agent(
|
|
419
|
+
model=model,
|
|
420
|
+
tools=[async_tool_with_injected_state],
|
|
421
|
+
state_schema=TestState,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Invoke with state data
|
|
425
|
+
result = await agent.ainvoke(
|
|
426
|
+
{
|
|
427
|
+
"messages": [HumanMessage("Test async")],
|
|
428
|
+
"internal_data": "secret_internal_value_xyz",
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Find the tool error message
|
|
433
|
+
tool_messages = [m for m in result["messages"] if m.type == "tool"]
|
|
434
|
+
assert len(tool_messages) == 1
|
|
435
|
+
tool_message = tool_messages[0]
|
|
436
|
+
assert tool_message.status == "error"
|
|
437
|
+
|
|
438
|
+
# Verify error mentions LLM-controlled parameters only
|
|
439
|
+
content = tool_message.content
|
|
440
|
+
assert "query" in content.lower(), "Error should mention 'query' (LLM-controlled)"
|
|
441
|
+
assert "max_results" in content.lower(), "Error should mention 'max_results' (LLM-controlled)"
|
|
442
|
+
|
|
443
|
+
# Verify system-injected state does not appear in the validation errors
|
|
444
|
+
# This keeps the error focused on what the LLM can actually fix
|
|
445
|
+
assert "internal_data" not in content, (
|
|
446
|
+
"Error should NOT mention 'internal_data' (system-injected field)"
|
|
447
|
+
)
|
|
448
|
+
assert "secret_internal_value" not in content, (
|
|
449
|
+
"Error should NOT contain system-injected state values"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Verify only LLM-controlled parameters are in the error list
|
|
453
|
+
# Should see "query" and "max_results" errors, but not "state"
|
|
454
|
+
lines = content.split("\n")
|
|
455
|
+
error_lines = [line.strip() for line in lines if line.strip()]
|
|
456
|
+
# Find lines that look like field names (single words at start of line)
|
|
457
|
+
field_errors = [
|
|
458
|
+
line
|
|
459
|
+
for line in error_lines
|
|
460
|
+
if line
|
|
461
|
+
and not line.startswith("input")
|
|
462
|
+
and not line.startswith("field")
|
|
463
|
+
and not line.startswith("error")
|
|
464
|
+
and not line.startswith("please")
|
|
465
|
+
and len(line.split()) <= 2
|
|
466
|
+
]
|
|
467
|
+
# Verify system-injected 'state' is not in the field error list
|
|
468
|
+
assert not any("state" == field.lower() for field in field_errors), (
|
|
469
|
+
"The field 'state' (system-injected) should not appear in validation errors"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
305
473
|
async def test_tool_node_error_handling() -> None:
|
|
306
474
|
def handle_all(e: ValueError | ToolException | ToolInvocationError):
|
|
307
475
|
return TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e))
|
|
@@ -355,10 +523,8 @@ async def test_tool_node_error_handling() -> None:
|
|
|
355
523
|
result_error["messages"][1].content
|
|
356
524
|
== f"Error: {ToolException('Test error')!r}\n Please fix your mistakes."
|
|
357
525
|
)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
or "validation error" in result_error["messages"][2].content
|
|
361
|
-
)
|
|
526
|
+
# Check that the validation error contains the field name
|
|
527
|
+
assert "some_other_val" in result_error["messages"][2].content
|
|
362
528
|
|
|
363
529
|
assert result_error["messages"][0].tool_call_id == "some id"
|
|
364
530
|
assert result_error["messages"][1].tool_call_id == "some other id"
|