Skip to content

Commit 89f6e59

Browse files
DeanChensjcopybara-github
authored andcommitted
chore: Unwraps callables in FunctionNode
FunctionNode now unwraps the callable before checking its type (async/sync, gen/coroutine). Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 929645733
1 parent ca8baf1 commit 89f6e59

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

src/google/adk/workflow/_function_node.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,11 @@ def __init__(
212212
' The node must rerun after credentials are provided.'
213213
)
214214

215-
inferred_name = name or getattr(func, '__name__', None)
215+
inferred_name = (
216+
name
217+
or getattr(func, '__name__', None)
218+
or getattr(_unwrap_callable(func), '__name__', None)
219+
)
216220
if not inferred_name:
217221
raise ValueError(
218222
'FunctionNode must have a name. If the wrapped callable does not'
@@ -497,9 +501,10 @@ async def _run_impl(
497501

498502
kwargs = self._bind_parameters(ctx, node_input)
499503

500-
if inspect.isasyncgenfunction(self._func):
504+
unwrapped_func = _unwrap_callable(self._func)
505+
if inspect.isasyncgenfunction(unwrapped_func):
501506
items = self._func(**kwargs)
502-
elif inspect.isgeneratorfunction(self._func):
507+
elif inspect.isgeneratorfunction(unwrapped_func):
503508
items = _sync_to_async_gen(self._func(**kwargs))
504509
else:
505510
items = None
@@ -510,7 +515,7 @@ async def _run_impl(
510515
if event is not None:
511516
yield event
512517
else:
513-
if inspect.iscoroutinefunction(self._func):
518+
if inspect.iscoroutinefunction(unwrapped_func):
514519
result = await self._func(**kwargs)
515520
else: # Sync function
516521
result = self._func(**kwargs)

tests/unittests/workflow/test_function_node.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,3 +1736,49 @@ def __call__(self, z: float) -> None:
17361736
obj = MyCallable()
17371737
hints4 = _get_type_hints_cached(obj)
17381738
assert hints4 == {'z': float, 'return': type(None)}
1739+
1740+
1741+
@pytest.mark.asyncio
1742+
async def test_function_node_wrapped_partial(request: pytest.FixtureRequest):
1743+
"""Tests that FunctionNode correctly unwraps functools.partial for async/sync generators and coroutines."""
1744+
import functools
1745+
1746+
async def async_gen_fn(
1747+
prefix: str, ctx: Context
1748+
) -> AsyncGenerator[Any, None]:
1749+
yield Event(output=f'{prefix} from AsyncGen')
1750+
1751+
def sync_gen_fn(prefix: str, ctx: Context) -> Generator[Any, None, None]:
1752+
yield Event(output=f'{prefix} from SyncGen')
1753+
1754+
async def async_fn(prefix: str, ctx: Context) -> str:
1755+
return f'{prefix} from AsyncCoro'
1756+
1757+
p_async_gen = functools.partial(async_gen_fn, 'Hello')
1758+
p_sync_gen = functools.partial(sync_gen_fn, 'Hello')
1759+
p_async = functools.partial(async_fn, 'Hello')
1760+
1761+
agent = Workflow(
1762+
name='test_workflow_partial_unwrapping',
1763+
edges=[
1764+
(START, p_async_gen),
1765+
(p_async_gen, p_sync_gen),
1766+
(p_sync_gen, p_async),
1767+
],
1768+
)
1769+
events, _, _ = await run_workflow(agent)
1770+
1771+
assert simplify_events_with_node(events) == [
1772+
(
1773+
'test_workflow_partial_unwrapping@1/async_gen_fn@1',
1774+
{'output': 'Hello from AsyncGen'},
1775+
),
1776+
(
1777+
'test_workflow_partial_unwrapping@1/sync_gen_fn@1',
1778+
{'output': 'Hello from SyncGen'},
1779+
),
1780+
(
1781+
'test_workflow_partial_unwrapping@1/async_fn@1',
1782+
{'output': 'Hello from AsyncCoro'},
1783+
),
1784+
]

0 commit comments

Comments
 (0)