Skip to content

fix(interop/langchain): route async Langchain tools through arun#2790

Closed
genisis0x wants to merge 4 commits into
ag2ai:mainfrom
genisis0x:fix/1402-langchain-async-tool
Closed

fix(interop/langchain): route async Langchain tools through arun#2790
genisis0x wants to merge 4 commits into
ag2ai:mainfrom
genisis0x:fix/1402-langchain-async-tool

Conversation

@genisis0x
Copy link
Copy Markdown
Contributor

Summary

Closes #1402.

LangChainInteroperability.convert_tool wraps every Langchain tool in a sync function that calls langchain_tool.run(...), even when the wrapped tool is async-native. Inside an await user.a_initiate_chat(...) flow this blocks the calling event loop and bypasses the tool's arun implementation.

This PR detects async-native tools and wraps them with a coroutine function that awaits arun. AG2's Tool already accepts coroutine functions as func_or_tool, so no further plumbing is needed.

Detection

Two cases are handled separately because Langchain models them differently:

  • StructuredTool / @tool-decorated tools — the only reliable signal is the coroutine attribute, which is None for sync wrappers and the original async function for async ones. These classes always override _arun, so a class-level _arun check would misclassify sync StructuredTools as async (this is what the first iteration of this patch did, and it broke TestLangChainInteroperabilityWithoutPydanticInput::test_convert_tool locally).
  • Direct BaseTool subclasses — no coroutine attribute. Fall back to checking whether _arun was overridden away from BaseTool's default thread-pool delegate and is a coroutine function.

Sync tools keep the existing sync wrapper unchanged.

Tests

pytest test/interop/langchain/test_langchain.py -v -k 'not test_with_llm'
...
7 passed, 2 deselected

New test classes:

  • TestLangChainInteroperabilityAsyncTool@langchain_tool on an async function produces an async wrapper (inspect.iscoroutinefunction(tool.func) is True) and awaiting it actually calls arun.
  • TestLangChainInteroperabilityBaseToolAsync — a custom BaseTool subclass that overrides only _arun is detected and routed through arun. Its sync _run raises RuntimeError, so the test fails if the wrapper falls back to run.

The two pre-existing sync tests (TestLangChainInteroperability::test_convert_tool and TestLangChainInteroperabilityWithoutPydanticInput::test_convert_tool) continue to pass, confirming sync tools still take the sync path.

Test plan

  • pytest test/interop/langchain/test_langchain.py -k 'not test_with_llm' — all green
  • Sync @langchain_tool still wrapped with a sync function (existing tests verify this)
  • Async @langchain_tool wrapped with a coroutine function and awaited via arun
  • Direct BaseTool subclass with overridden _arun routed through arun

LangChainInteroperability.convert_tool wrapped every Langchain tool
in a sync function that called langchain_tool.run(...). For async-native
tools this blocked the calling event loop and discarded the tool's
arun implementation, even when AG2 was running under a_initiate_chat.

Detect async-native tools and wrap them with a coroutine function that
awaits arun:

* StructuredTool / Tool: rely on the `coroutine` attribute, which is
  None for sync wrappers and the original async function for async
  ones. The class-level _arun is always overridden, so checking _arun
  alone misclassifies sync StructuredTools.
* Direct BaseTool subclasses: no `coroutine` attribute, so fall back
  to checking whether `_arun` was overridden away from BaseTool's
  default thread-pool delegate and is a coroutine function.

Sync tools keep the existing wrapper untouched. AG2's Tool already
accepts coroutine functions as func_or_tool, so the async path needs
no further plumbing.

Test coverage:
* TestLangChainInteroperabilityAsyncTool: @langchain_tool on an async
  function produces an async wrapper and invoking it calls arun.
* TestLangChainInteroperabilityBaseToolAsync: a custom BaseTool that
  overrides only _arun is detected and routed through arun; its sync
  _run raises so the test fails if the wrapper falls back to .run.

Closes ag2ai#1402
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 8, 2026

CLA assistant check
All committers have signed the CLA.

vvlrff and others added 3 commits May 11, 2026 18:43
…typos

Resolve CI failures on PR ag2ai#2790:

- mypy `[misc]` "All conditional function variants must have identical
  signatures" on the inline `if/else` defining `func` twice. Replace the
  inline redefinition with two distinct nested callables (`_async_func`,
  `_sync_func`) and pick one based on `_is_async_langchain_tool`.

- typos hook flagged `mis-classify` in the helper docstring. Replace
  with `misclassify` (one word, not in the typo dictionary).

- ruff auto-moves the `Callable` import from `typing` to
  `collections.abc` per project rules.

Behaviour is unchanged. The existing test classes already exercise both
branches: sync StructuredTool, async StructuredTool, BaseTool subclass
with overridden `_arun`.
…error paths

Codecov flagged the original PR at 16.66% patch coverage. Add three
tests that lift that to 96% locally and exercise every branch the fix
introduced:

- TestLangChainInteroperabilityBaseToolSync — direct BaseTool subclass
  with only _run overridden. Pins the False branch of
  _is_async_langchain_tool: no `coroutine` attribute, _arun unchanged
  from the BaseTool default, so convert_tool must fall back to the
  synchronous wrapper and the resulting Tool.func must be a plain
  function rather than a coroutine function.

- TestLangChainInteroperabilityConvertToolErrors::test_rejects_non_langchain_tool
  exercises the first ValueError guard.

- TestLangChainInteroperabilityConvertToolErrors::test_rejects_extra_kwargs
  exercises the second ValueError guard.

Only the get_unsupported_reason fallback (returned when langchain is
not installed) remains uncovered locally; covering it requires
unloading langchain from sys.modules, which would interfere with the
sibling tests in this file.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

❌ Patch coverage is 25.00000% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
autogen/interop/langchain/langchain_tool.py 25.00% 9 Missing ⚠️
Files with missing lines Coverage Δ
autogen/interop/langchain/langchain_tool.py 53.65% <25.00%> (-9.68%) ⬇️

... and 154 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.

@marklysze
Copy link
Copy Markdown
Collaborator

Thanks for the fix — the _is_async_langchain_tool helper is a clean approach with good handling of both the StructuredTool.coroutine path and the _arun override path.

Flagging a conflict: PR #2806 (by @obchain) covers the same fix to autogen/interop/langchain/langchain_tool.py and has already been approved. Both PRs touch the same files so they can't both merge. Please check if one of these should be closed in favour of the other.

@marklysze
Copy link
Copy Markdown
Collaborator

Thanks for the async Langchain fix — it addresses the same issue as PR #2806 which was opened shortly before and has already been approved.

PR #2806 also includes tests that cover the async path via the interop from_langchain wrapper and a mock async tool. Once #2806 merges, this PR will close automatically via the issue link if you have the Closes syntax set up, or you can close it manually.

No action needed on your end — this is just a heads-up that the fix is already in flight.

Copy link
Copy Markdown
Collaborator

@marklysze marklysze left a comment

Choose a reason for hiding this comment

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

Clean fix for #1402. The two-case detection in _is_async_langchain_tool is the right approach:

  1. StructuredTool/Tool path: checking tool.coroutine is not None is the reliable signal — the class always overrides _arun, so a plain MRO check would misclassify sync-wrapped tools as async.
  2. BaseTool subclass path: comparing cls_arun is not base_arun + iscoroutinefunction is precise and avoids false positives from the default delegating _arun.

Note: PR #2618 (Ricardo-M-L, April 11) addresses the same issue but uses MRO-only detection which misses the StructuredTool case. This implementation is more complete.

CI is clean and tests cover both paths. ✓

Ricardo-M-L added a commit to Ricardo-M-L/ag2 that referenced this pull request May 20, 2026
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`.
@vvlrff
Copy link
Copy Markdown
Collaborator

vvlrff commented May 22, 2026

@genisis0x Thanks for the pull request! This PR #2806 closes the issue.

@vvlrff vvlrff closed this May 22, 2026
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

4 participants