Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/crewai/src/crewai/tools/structured_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,16 @@ async def ainvoke(
return await self.func(**parsed_args, **kwargs)
import asyncio

return await asyncio.get_event_loop().run_in_executor(
result = await asyncio.get_event_loop().run_in_executor(
None, lambda: self.func(**parsed_args, **kwargs)
)
# A sync wrapper around an async tool (e.g. BaseTool.to_structured_tool
# storing Tool._run as func) hands a bare coroutine back from the
# executor; awaiting it here keeps the caller from receiving the
# coroutine object itself.
if inspect.isawaitable(result):
return await result
return result
except Exception:
raise

Expand Down
39 changes: 39 additions & 0 deletions lib/crewai/tests/tools/test_structured_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,42 @@ def failing_function(should_fail: bool) -> str:
tool.invoke({"should_fail": True})

assert call_count == 1


@pytest.mark.asyncio
async def test_ainvoke_awaits_coroutine_from_sync_wrapper():
"""ainvoke must await a coroutine handed back by a sync wrapper function."""
calls: list[str] = []

async def async_body(q: str) -> str:
calls.append(q)
return f"data:{q}"

def sync_wrapper(q: str) -> object:
"""Sync wrapper that returns the coroutine from async_body."""
return async_body(q)

tool = CrewStructuredTool.from_function(func=sync_wrapper, name="wrapped_async_tool")

result = await tool.ainvoke(input={"q": "hello"})

assert result == "data:hello"
assert calls == ["hello"]


@pytest.mark.asyncio
async def test_ainvoke_through_to_structured_tool_resolves_async_tool_result():
"""An @tool-decorated async function wraps the sync Tool._run wrapper as func; ainvoke
must resolve the coroutine it returns instead of handing the caller a bare coroutine."""
from crewai.tools import tool

@tool
async def fetch_data(q: str) -> str:
"""Fetch data for q."""
return f"data:{q}"

structured = fetch_data.to_structured_tool()

result = await structured.ainvoke(input={"q": "hello"})

assert result == "data:hello"