fix: support async LangChain tools in convert_tool()#2618
Conversation
…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>
|
@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).
|
@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:
Verified locally with mypy 1.17.1 (matches the CI pin). CI is now green (16/16). Ready for another look 🙏 |
|
Thanks for this fix — the MRO-walk detection is clean. One gap: LangChain's PR #2790 (genisis0x) handles this correctly by checking 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`.
|
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):
if hasattr(langchain_tool, "coroutine"):
return getattr(langchain_tool, "coroutine") is not None
# fall through to MRO walkTwo regression tests added to pin both StructuredTool paths:
Happy to address anything else 🙏 |
Codecov Report❌ Patch coverage is
... and 282 files with indirect coverage changes 🚀 New features to boost your workflow:
|
|
@Ricardo-M-L Thanks for the pull request! This PR #2806 closes the issue. |
Summary
Fixes #1402
LangChainInteroperability.convert_tool()always wraps LangChain tools with a synchronous function that callslangchain_tool.run(), even when the tool provides native async support via_arun. This blocks the event loop when used insidea_initiate_chat()and other async execution paths._arun(i.e., provides a real async implementation beyond BaseTool's defaultNotImplementedErrorstub), the converted tool now uses anasync deffunction that callsainvoke(). Tools without async support continue to use the synchronousrun()method, preserving full backward compatibility.a_execute_function()already checksis_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:_langchain_tool_has_async_implementation()helper that walks the MRO to detect if the tool's_arunis overridden fromBaseTool's defaultconvert_tool()to create an async wrapper usingainvoke()when async support is detectedtest/interop/langchain/test_langchain_async.py(new):await_langchain_tool_has_async_implementationdetection helperTest plan
pytest test/interop/langchain/test_langchain_async.py— new async tests passpytest test/interop/langchain/test_langchain.py— existing sync tests still pass (regression)_arunoverride🤖 Generated with Claude Code