Skip to content

fix: support async LangChain tools in convert_tool()#2618

Closed
Ricardo-M-L wants to merge 4 commits into
ag2ai:mainfrom
Ricardo-M-L:fix/langchain-async-tool-support
Closed

fix: support async LangChain tools in convert_tool()#2618
Ricardo-M-L wants to merge 4 commits into
ag2ai:mainfrom
Ricardo-M-L:fix/langchain-async-tool-support

Conversation

@Ricardo-M-L
Copy link
Copy Markdown
Contributor

Summary

Fixes #1402

  • Problem: LangChainInteroperability.convert_tool() always wraps LangChain tools with a synchronous function that calls langchain_tool.run(), even when the tool provides native async support via _arun. This blocks the event loop when used inside a_initiate_chat() and other async execution paths.
  • Solution: Added async detection via MRO inspection. When a LangChain tool overrides _arun (i.e., provides a real async implementation beyond BaseTool's default NotImplementedError stub), the converted tool now uses an async def function that calls ainvoke(). Tools without async support continue to use the synchronous run() method, preserving full backward compatibility.
  • How it works with the framework: AG2's a_execute_function() already checks is_coroutine_callable(func) and properly awaits async functions. By providing an async function when the LangChain tool supports it, the tool integrates naturally with the existing async execution path.

Changes

autogen/interop/langchain/langchain_tool.py:

  • Added _langchain_tool_has_async_implementation() helper that walks the MRO to detect if the tool's _arun is overridden from BaseTool's default
  • Modified convert_tool() to create an async wrapper using ainvoke() when async support is detected
  • Sync path remains unchanged for backward compatibility

test/interop/langchain/test_langchain_async.py (new):

  • Tests that sync-only tools remain sync after conversion
  • Tests that async tools produce async converted functions
  • Tests async tool execution via await
  • Tests concurrent execution (async tool doesn't block event loop)
  • Tests metadata preservation, error handling
  • Tests for the _langchain_tool_has_async_implementation detection helper

Test plan

  • pytest test/interop/langchain/test_langchain_async.py — new async tests pass
  • pytest test/interop/langchain/test_langchain.py — existing sync tests still pass (regression)
  • Verify async detection correctly identifies tools with/without _arun override

🤖 Generated with Claude Code

Ricardo-M-L and others added 2 commits April 12, 2026 00:19
…rt_tool()

When a LangChain tool provides a native async implementation (overrides
_arun), the converted AG2 Tool now uses an async function that calls
ainvoke() instead of the synchronous run(). This allows the tool to be
properly awaited inside a_initiate_chat() and other async execution
paths, preventing event loop blocking.

Tools without async support continue to use the synchronous run() method,
preserving full backward compatibility.

Fixes ag2ai#1402
The mypy strict mode flags langchain types as Any due to no type stubs.
Add type ignores following the existing pattern in langchain_tool.py
and replace LangchainBaseTool annotations in test fixtures with Any.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@marklysze
Copy link
Copy Markdown
Collaborator

@Ricardo-M-L thanks for putting this together, would you mind checking the test failures.

The previous fix covered TestLangChainAsyncToolConversion but missed
the second test class TestLangChainAsyncDetection. mypy still flags
two LangchainBaseTool subclasses (lines 169, 202) and the @langchain_tool
decorator (line 186) because the optional langchain_core import resolves
to Any.

Apply the same `# type: ignore[misc, no-any-unimported]` and
`# type: ignore[misc]` pattern used elsewhere in this file.

Verified locally with mypy 1.17.1 (matches CI pin).
@Ricardo-M-L
Copy link
Copy Markdown
Contributor Author

@marklysze The type-check failures are fixed in 325b324. The previous fix added `# type: ignore` markers to `TestLangChainAsyncToolConversion` but missed the second class `TestLangChainAsyncDetection` (lines 169 / 186 / 202). Same pattern applied:

  • `# type: ignore[misc, no-any-unimported]` for `LangchainBaseTool` subclasses
  • `# type: ignore[misc]` for the `@langchain_tool` decorator

Verified locally with mypy 1.17.1 (matches the CI pin). CI is now green (16/16). Ready for another look 🙏

@marklysze
Copy link
Copy Markdown
Collaborator

Thanks for this fix — the MRO-walk detection is clean.

One gap: LangChain's StructuredTool / Tool always override _arun at the class level (it delegates back to the passed coroutine, or falls through to run in a thread if coroutine is None). The MRO check therefore classifies all StructuredTool-wrapped sync functions as async-native, which would route them through ainvoke unnecessarily.

PR #2790 (genisis0x) handles this correctly by checking tool.coroutine is not None for the StructuredTool path before falling back to the MRO check. That two-case detection avoids the false-positive.

This PR was opened first and the core approach is right, but the StructuredTool gap would need addressing before merging. #2790's detection logic may be worth adopting.

Review feedback on ag2ai#2618 (thanks @marklysze): the MRO-only check
misclassified every callable wrapped with `@langchain_core.tools.tool`
as async, because `StructuredTool` / `Tool` always override `_arun` at
the class level — the override internally delegates to the `coroutine`
attribute (None for sync callables, the async function for async ones).
Sync wrappers therefore got routed through `ainvoke` unnecessarily.

Split `_langchain_tool_has_async_implementation` into two paths:

1. StructuredTool / Tool — check `tool.coroutine is not None` directly.
   This is the only reliable signal; the class-level `_arun` override
   would otherwise force a false positive.
2. BaseTool subclasses — keep the existing MRO walk (the original
   case the PR was written for).

Same two-case detection as ag2ai#2790's `_is_async_langchain_tool` helper.

Adds two regression tests:
- `test_structured_tool_sync_wrapped_callable_is_not_async` — fails on
  the old MRO-only code, passes here.
- `test_structured_tool_async_wrapped_callable_is_async` — pins that
  async wrappers stay correctly classified via `coroutine`.
@Ricardo-M-L
Copy link
Copy Markdown
Contributor Author

Thanks @marklysze — you're right, the StructuredTool path needed its own case. Just pushed the fix:

Detection now splits into two cases (same shape as #2790):

  1. StructuredTool / Tool — check tool.coroutine is not None directly. The class-level _arun override would otherwise force a false positive for every sync wrapper.
  2. BaseTool subclasses — keep the MRO walk (the original case the PR was written for).
if hasattr(langchain_tool, "coroutine"):
    return getattr(langchain_tool, "coroutine") is not None
# fall through to MRO walk

Two regression tests added to pin both StructuredTool paths:

  • test_structured_tool_sync_wrapped_callable_is_not_async — fails on the old MRO-only code (was the gap you flagged), passes here.
  • test_structured_tool_async_wrapped_callable_is_async — pins that async wrappers stay correctly classified via coroutine.

Happy to address anything else 🙏

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

❌ Patch coverage is 5.88235% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
autogen/interop/langchain/langchain_tool.py 5.88% 16 Missing ⚠️
Files with missing lines Coverage Δ
autogen/interop/langchain/langchain_tool.py 42.55% <5.88%> (-20.79%) ⬇️

... and 282 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vvlrff
Copy link
Copy Markdown
Collaborator

vvlrff commented May 22, 2026

@Ricardo-M-L Thanks for the pull request! This PR #2806 closes the issue.

@vvlrff vvlrff closed this May 22, 2026
@Ricardo-M-L
Copy link
Copy Markdown
Contributor Author

Thanks @vvlrff — confirmed #2806 landed yesterday and covers the same case (the coroutine is not None check + MRO walk split is the exact resolution this PR converged on after your earlier review note). Closing this since the issue is fixed upstream — no point keeping a duplicate open. 🙏

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.

[Bug]: Synchronous Call of Asynchronous Langchain Tool in Autogen Interoperability System

3 participants