Skip to content

Commit 2d983ec

Browse files
committed
fix: run sync tools off event loop
1 parent 939d4d0 commit 2d983ec

2 files changed

Lines changed: 36 additions & 4 deletions

File tree

python/packages/core/agent_framework/_tools.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,15 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any:
537537
self.invocation_exception_count += 1
538538
raise
539539

540+
async def _invoke_function(self, call_kwargs: Mapping[str, Any]) -> Any:
541+
"""Run sync tools off the event loop during async invocation."""
542+
func = self.func.func if isinstance(self.func, FunctionTool) else self.func
543+
if inspect.iscoroutinefunction(func):
544+
return await self.__call__(**call_kwargs)
545+
546+
res = await asyncio.to_thread(self.__call__, **call_kwargs)
547+
return await res if inspect.isawaitable(res) else res
548+
540549
@overload
541550
async def invoke(
542551
self,
@@ -679,8 +688,7 @@ async def invoke(
679688
if not OBSERVABILITY_SETTINGS.ENABLED: # type: ignore[name-defined]
680689
logger.info(f"Function name: {self.name}")
681690
logger.debug(f"Function arguments: {observable_kwargs}")
682-
res = self.__call__(**call_kwargs)
683-
result = await res if inspect.isawaitable(res) else res
691+
result = await self._invoke_function(call_kwargs)
684692
if skip_parsing:
685693
logger.info(f"Function {self.name} succeeded.")
686694
logger.debug(f"Function result: {type(result).__name__}")
@@ -730,8 +738,7 @@ async def invoke(
730738
start_time_stamp = perf_counter()
731739
end_time_stamp: float | None = None
732740
try:
733-
res = self.__call__(**call_kwargs)
734-
result = await res if inspect.isawaitable(res) else res
741+
result = await self._invoke_function(call_kwargs)
735742
end_time_stamp = perf_counter()
736743
except Exception as exception:
737744
end_time_stamp = perf_counter()

python/packages/core/tests/core/test_tools.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Copyright (c) Microsoft. All rights reserved.
2+
import asyncio
3+
import threading
24
from typing import Annotated, Any, Literal, get_args, get_origin
35
from unittest.mock import Mock
46

@@ -1346,6 +1348,29 @@ async def slow(x: int) -> int:
13461348
assert raw == 42
13471349

13481350

1351+
async def test_invoke_sync_tool_does_not_block_event_loop() -> None:
1352+
release_tool = threading.Event()
1353+
tool_thread_ids: list[int] = []
1354+
event_loop_thread_id = threading.get_ident()
1355+
1356+
@tool
1357+
def wait_for_release() -> str:
1358+
tool_thread_ids.append(threading.get_ident())
1359+
return "released" if release_tool.wait(timeout=0.2) else "timed out"
1360+
1361+
async def release_soon() -> None:
1362+
await asyncio.sleep(0.01)
1363+
release_tool.set()
1364+
1365+
tool_task = asyncio.create_task(wait_for_release.invoke(skip_parsing=True))
1366+
release_task = asyncio.create_task(release_soon())
1367+
1368+
assert await asyncio.wait_for(tool_task, timeout=1) == "released"
1369+
await release_task
1370+
assert tool_thread_ids
1371+
assert tool_thread_ids[0] != event_loop_thread_id
1372+
1373+
13491374
async def test_invoke_skip_parsing_bypasses_configured_result_parser() -> None:
13501375
"""The tool's own result_parser is bypassed when skip_parsing=True is requested."""
13511376
parser_calls: list[Any] = []

0 commit comments

Comments
 (0)