use _get_type_hints

This commit is contained in:
Chester Curme committed 2026-08-11 11:56:32 -04:00
1 parent 39b4e0f9a8
commit bbb105dfe1
2 files changed
+71 -2

No files matched your search

+12 -2
View File
@@ -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))
)
+59
View File
@@ -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`.