From bbb105dfe1d7caf37bcb09e235b3f8de460c07d7 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Tue, 11 Aug 2026 11:56:32 -0400 Subject: [PATCH] use _get_type_hints --- libs/core/langchain_core/tools/structured.py | 14 ++++- libs/core/tests/unit_tests/test_tools.py | 59 ++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/libs/core/langchain_core/tools/structured.py b/libs/core/langchain_core/tools/structured.py index e9643a0ad8..cba45463d4 100644 --- a/libs/core/langchain_core/tools/structured.py +++ b/libs/core/langchain_core/tools/structured.py @@ -28,6 +28,7 @@ from langchain_core.tools.base import ( ArgsSchema, BaseTool, _get_runnable_config_param, + _get_type_hints, _is_injected_arg_type, create_schema_from_function, ) @@ -256,10 +257,19 @@ class StructuredTool(BaseTool): fn = self.func or self.coroutine if fn is None: return _EMPTY_SET + # Resolve annotations via `_get_type_hints` (rather than reading raw + # `signature` annotations) so postponed annotations -- e.g. from + # `from __future__ import annotations` or quoted forward references -- + # are recognized. `include_extras=True` preserves `Annotated` metadata + # so `InjectedToolArg` markers survive resolution. Fall back to the raw + # per-parameter annotation when a hint can't be resolved. Mirrors + # `BaseTool._injected_args_keys` in `base.py`. + params = signature(fn).parameters + hints = _get_type_hints(fn, include_extras=True) or {} return frozenset( k - for k, v in signature(fn).parameters.items() - if _is_injected_arg_type(v.annotation) + for k, v in params.items() + if _is_injected_arg_type(hints.get(k, v.annotation)) ) diff --git a/libs/core/tests/unit_tests/test_tools.py b/libs/core/tests/unit_tests/test_tools.py index f23fd9052a..1751627d9e 100644 --- a/libs/core/tests/unit_tests/test_tools.py +++ b/libs/core/tests/unit_tests/test_tools.py @@ -3781,6 +3781,65 @@ def test_base_tool_subclass_injects_postponed_annotated_arg() -> None: assert captured["injected"] == "value" +def test_structured_tool_injects_postponed_annotation_runtime() -> None: + """`StructuredTool` must resolve postponed / forward-ref injected args too. + + `StructuredTool._injected_args_keys` inspects the wrapped `func`/`coroutine` + instead of `_run`, so it needs the same hint resolution as + `BaseTool._injected_args_keys`. A quoted forward reference exposes a raw + string annotation at `signature()` time; without resolution the injected + arg is omitted from `_injected_args_keys` and the call raises `TypeError`. + Fixes #39568. + """ + + class MultiplyInput(BaseModel): + a: int + b: int + + captured: dict[str, Any] = {} + + # Quoted forward reference -> raw string annotation "_CustomRuntime". + @tool(args_schema=MultiplyInput) + def multiply(a: int, b: int, runtime: "_CustomRuntime") -> int: + """Multiply two numbers.""" + captured["runtime"] = runtime + return a * b + + assert "runtime" in multiply._injected_args_keys + + runtime = _CustomRuntime(data={"scale": 10}) + result = multiply.invoke({"a": 2, "b": 3, "runtime": runtime}) + assert result == 6 + assert captured["runtime"] is runtime + + +def test_structured_tool_injects_postponed_annotated_arg() -> None: + """`StructuredTool` must resolve postponed `Annotated[..., InjectedToolArg]`. + + `include_extras=True` when resolving hints preserves the `InjectedToolArg` + metadata, so annotated injected args on the wrapped function are still + detected. Fixes #39568. + """ + + class QueryInput(BaseModel): + query: str + + captured: dict[str, Any] = {} + + # Quoted forward reference to a postponed `Annotated` injected arg. + @tool(args_schema=QueryInput) + def echo(query: str, injected: "Annotated[str, InjectedToolArg]") -> str: + """Echo the query.""" + captured["injected"] = injected + return query + + assert "injected" in echo._injected_args_keys + + result = echo.invoke({"query": "hi", "injected": "value"}) + assert result == "hi" + assert captured["injected"] == "value" + + class CallbackHandlerWithToolCallIdCapture(FakeCallbackHandler): """Callback handler that captures `tool_call_id` passed to `on_tool_start`.