Skip to content

fix(integrations): use the wrapped function of a plain langchain Tool - #8036

Open
HhH666-ux wants to merge 2 commits into
google:mainfrom
HhH666-ux:fix/langchain-plain-tool-func
Open

HhH666-ux wants to merge 2 commits into
google:mainfrom
HhH666-ux:fix/langchain-plain-tool-func

Conversation

@HhH666-ux

Copy link
Copy Markdown

Fixes #8035.

Problem

LangchainTool.__init__ only unwraps StructuredTool.func (the #707 fix). For a plain langchain_core.tools.Tool, the fallback lands on Tool._run(self, *args, config: RunnableConfig, ...), and FunctionTool.__init__ derives the function declaration from that signature — so the model sees a single required config parameter:

Tool declares: ['config'] required: ['config']
Tool run_async: {'error': 'Invoking `shout()` failed as the following mandatory input parameters are not present: config ...'}

while a StructuredTool built from the same function declares ['text'] and runs.

Changes

Before the generic fallback, unwrap a plain Tool's real callable:

if isinstance(tool, Tool) and getattr(tool, 'func', None) is not None:
  func = tool.func
elif hasattr(tool, '_run') or hasattr(tool, 'run'):
  ...

This mirrors the StructuredTool precedence (func first): the declaration is built from the user function's signature, and invocation goes through the same callable the StructuredTool path already uses. Scoped strictly to Tool instances — custom BaseTool subclasses that intentionally override _run keep the current behavior, and Tool(coroutine=...) without func is unchanged.

Verification

Offline, no API key (the issue's script, langchain-core 1.7.x):

  • before: Tool(name="shout", func=shout) declares ['config'] required ['config']
  • after: declares ['text'] required ['text'], matching the StructuredTool.from_function(shout) control

LangchainTool's func fallback only unwrapped StructuredTool.func (the google#707
fix). For a plain Tool it landed on Tool._run(self, *args, config, ...), so
FunctionTool derived a declaration with a single required "config"
parameter the model is asked for and the invocation always failed with the
mandatory-parameters error. Unwrap Tool.func before the generic fallback,
mirroring the StructuredTool precedence; custom BaseTool subclasses that
override _run keep the current behavior.

Fixes google#8035
@google-cla

google-cla Bot commented Nov 8, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Comment on lines +94 to +95
if isinstance(tool, Tool) and getattr(tool, 'func', None) is not None:
func = tool.func

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a plain Tool(func=None, coroutine=afn) still falls through to _run here, so it still declares config and then raises "Tool does not support sync invocation". the StructuredTool branch above already falls back to the coroutine, same thing works here

Suggested change
if isinstance(tool, Tool) and getattr(tool, 'func', None) is not None:
func = tool.func
if isinstance(tool, Tool) and (tool.func or tool.coroutine) is not None:
func = tool.func or tool.coroutine

fixed this with tests for both the sync and async plain Tool in this commit, you can just cherry pick it instead of redoing it: its-amann@5642875

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the func is not None check still landed on _run for Tool(func=None, coroutine=afn).

Pushed b29f889: the plain Tool branch now mirrors the StructuredTool one (tool.func or tool.coroutine), so an async-only plain Tool no longer advertises config or raises "Tool does not support sync invocation".

I took your test cases as-is (test_plain_tool_with_func / test_plain_tool_with_coroutine); all 12 tests in tests/unittests/integrations/langchain/test_langchain_tool.py pass locally. Thanks also for preparing the cherry-pick commit — much appreciated.

@chrikrah chrikrah left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HhH666-ux the func branch fixes the Tool(func=...) case from #8035, but I'd hold the merge for a test, since the 10 existing ones also pass on the base. blocking: its-amann's 5642875 adds that test plus the coroutine fallback you scoped out. Both tests fail on the base, and the coroutine one fails here too.

non-blocking: they check only the run_async result; asserting the declaration is ['text'] would pin what the model sees.

# git archive 912d7c6 and 5642875 (base 1cb926d), Python 3.12, langchain-core 1.7.6, pytest 9.2.0, langchain_tool.__file__ from the export
$ python repro.py   # the issue's script plus a coroutine-only Tool
head  Tool:            declares ['text'], run_async HI
head  Tool(coroutine): declares ['config'], run_async error: mandatory input parameters are not present: config
base  Tool:            declares ['config'], run_async error: mandatory input parameters are not present: config
$ python -m pytest -q tests/unittests/integrations/langchain/test_langchain_tool.py   # tests from 5642875, langchain_tool.py per row
base 1cb926d:  2 failed, 10 passed   # test_plain_tool_with_func, test_plain_tool_with_coroutine
head 912d7c6:  1 failed, 11 passed   # test_plain_tool_with_coroutine
5642875:       12 passed

Whichever branch ends up carrying 5642875, I'll rerun this there and approve.

A plain Tool built as Tool(func=None, coroutine=afn) only has a coroutine,
so the previous check fell through to Tool._run and advertised a required
"config" parameter before failing with "Tool does not support sync
invocation". Mirror the StructuredTool branch: prefer func, then
coroutine. Tests cover both the sync and async plain Tool paths.

Addresses the review on this PR (thanks @its-amann).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LangchainTool asks the model for config when it wraps a plain langchain Tool

4 participants