Skip to content

Commit f83c39f

Browse files
madebygpsCopilot
andauthored
Python: fix: resolve string annotations in FunctionExecutor (#2308)
* fix: resolve string annotations in FunctionExecutor Enhance type hint validation in FunctionExecutor by importing `typing` and using `get_type_hints` to correctly resolve annotations. This fixes validation failures when `from __future__ import annotations` is enabled, which stores annotations as strings. Fixes #1808 * Update python/packages/core/tests/workflow/test_function_executor_future.py Co-authored-by: Copilot <[email protected]> * ran pre commit --------- Co-authored-by: Copilot <[email protected]>
1 parent 037349f commit f83c39f

File tree

2 files changed

+44
-3
lines changed

2 files changed

+44
-3
lines changed

python/packages/core/agent_framework/_workflows/_function_executor.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import asyncio
1919
import inspect
20+
import typing
2021
from collections.abc import Awaitable, Callable
2122
from typing import Any, overload
2223

@@ -218,15 +219,16 @@ def _validate_function_signature(func: Callable[..., Any]) -> tuple[type, Any, l
218219
if message_param.annotation == inspect.Parameter.empty:
219220
raise ValueError(f"Function instance {func.__name__} must have a type annotation for the message parameter")
220221

221-
message_type = message_param.annotation
222+
type_hints = typing.get_type_hints(func)
223+
message_type = type_hints.get(message_param.name, message_param.annotation)
222224

223225
# Check if there's a context parameter
224226
if len(params) == 2:
225227
ctx_param = params[1]
228+
ctx_annotation = type_hints.get(ctx_param.name, ctx_param.annotation)
226229
output_types, workflow_output_types = validate_workflow_context_annotation(
227-
ctx_param.annotation, f"parameter '{ctx_param.name}'", "Function instance"
230+
ctx_annotation, f"parameter '{ctx_param.name}'", "Function instance"
228231
)
229-
ctx_annotation = ctx_param.annotation
230232
else:
231233
# No context parameter (only valid for function executors)
232234
output_types, workflow_output_types = [], []
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from agent_framework import FunctionExecutor, WorkflowContext, executor
8+
9+
10+
class TestFunctionExecutorFutureAnnotations:
11+
"""Test suite for FunctionExecutor with from __future__ import annotations."""
12+
13+
def test_executor_decorator_future_annotations(self):
14+
"""Test @executor decorator works with stringified annotations."""
15+
16+
@executor(id="future_test")
17+
async def process_future(value: int, ctx: WorkflowContext[int]) -> None:
18+
await ctx.send_message(value * 2)
19+
20+
assert isinstance(process_future, FunctionExecutor)
21+
assert process_future.id == "future_test"
22+
assert int in process_future._handlers
23+
24+
# Check spec
25+
spec = process_future._handler_specs[0]
26+
assert spec["message_type"] is int
27+
assert spec["output_types"] == [int]
28+
29+
def test_executor_decorator_future_annotations_complex(self):
30+
"""Test @executor decorator works with complex stringified annotations."""
31+
32+
@executor
33+
async def process_complex(data: dict[str, Any], ctx: WorkflowContext[list[str]]) -> None:
34+
await ctx.send_message(["done"])
35+
36+
assert isinstance(process_complex, FunctionExecutor)
37+
spec = process_complex._handler_specs[0]
38+
assert spec["message_type"] == dict[str, Any]
39+
assert spec["output_types"] == [list[str]]

0 commit comments

Comments
 (0)