From ac626d3038f01eb2ee2f6cda7aa6c5961e90064e Mon Sep 17 00:00:00 2001 From: Gwyneth Pena-Siguenza Date: Tue, 18 Nov 2025 14:55:20 -0500 Subject: [PATCH 1/3] 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 --- .../_workflows/_function_executor.py | 8 ++-- .../workflow/test_function_executor_future.py | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 python/packages/core/tests/workflow/test_function_executor_future.py diff --git a/python/packages/core/agent_framework/_workflows/_function_executor.py b/python/packages/core/agent_framework/_workflows/_function_executor.py index f79d85b1a7..417a4ee51b 100644 --- a/python/packages/core/agent_framework/_workflows/_function_executor.py +++ b/python/packages/core/agent_framework/_workflows/_function_executor.py @@ -17,6 +17,7 @@ import asyncio import inspect +import typing from collections.abc import Awaitable, Callable from typing import Any, overload @@ -218,15 +219,16 @@ def _validate_function_signature(func: Callable[..., Any]) -> tuple[type, Any, l if message_param.annotation == inspect.Parameter.empty: raise ValueError(f"Function instance {func.__name__} must have a type annotation for the message parameter") - message_type = message_param.annotation + type_hints = typing.get_type_hints(func) + message_type = type_hints.get(message_param.name, message_param.annotation) # Check if there's a context parameter if len(params) == 2: ctx_param = params[1] + ctx_annotation = type_hints.get(ctx_param.name, ctx_param.annotation) output_types, workflow_output_types = validate_workflow_context_annotation( - ctx_param.annotation, f"parameter '{ctx_param.name}'", "Function instance" + ctx_annotation, f"parameter '{ctx_param.name}'", "Function instance" ) - ctx_annotation = ctx_param.annotation else: # No context parameter (only valid for function executors) output_types, workflow_output_types = [], [] diff --git a/python/packages/core/tests/workflow/test_function_executor_future.py b/python/packages/core/tests/workflow/test_function_executor_future.py new file mode 100644 index 0000000000..7b2aea1577 --- /dev/null +++ b/python/packages/core/tests/workflow/test_function_executor_future.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +from typing import Any + +from agent_framework import FunctionExecutor, WorkflowContext, executor + + +class TestFunctionExecutorFutureAnnotations: + """Test suite for FunctionExecutor with from __future__ import annotations.""" + + def test_executor_decorator_future_annotations(self): + """Test @executor decorator works with stringified annotations.""" + + @executor(id="future_test") + async def process_future(value: int, ctx: WorkflowContext[int]) -> None: + await ctx.send_message(value * 2) + + assert isinstance(process_future, FunctionExecutor) + assert process_future.id == "future_test" + assert int in process_future._handlers + + # Check spec + spec = process_future._handler_specs[0] + assert spec["message_type"] is int + assert spec["output_types"] == [int] + + def test_executor_decorator_future_annotations_complex(self): + """Test @executor decorator works with complex stringified annotations.""" + + @executor + async def process_complex( + data: dict[str, Any], ctx: WorkflowContext[list[str]] + ) -> None: + await ctx.send_message(["done"]) + + assert isinstance(process_complex, FunctionExecutor) + + spec = process_complex._handler_specs[0] + assert spec["message_type"] == dict[str, Any] + assert spec["output_types"] == [list[str]] From ac54a7f4775c1d01c2214dede556d3acfda5f784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwyneth=20Pe=C3=B1a-Siguenza?= Date: Tue, 18 Nov 2025 15:55:23 -0500 Subject: [PATCH 2/3] Update python/packages/core/tests/workflow/test_function_executor_future.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../core/tests/workflow/test_function_executor_future.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/packages/core/tests/workflow/test_function_executor_future.py b/python/packages/core/tests/workflow/test_function_executor_future.py index 7b2aea1577..3926536c02 100644 --- a/python/packages/core/tests/workflow/test_function_executor_future.py +++ b/python/packages/core/tests/workflow/test_function_executor_future.py @@ -36,7 +36,6 @@ async def process_complex( await ctx.send_message(["done"]) assert isinstance(process_complex, FunctionExecutor) - spec = process_complex._handler_specs[0] assert spec["message_type"] == dict[str, Any] assert spec["output_types"] == [list[str]] From 5e64db0c2d0d013907b8246c8da2b2f15b9567e7 Mon Sep 17 00:00:00 2001 From: Gwyneth Pena-Siguenza Date: Tue, 18 Nov 2025 18:39:00 -0500 Subject: [PATCH 3/3] ran pre commit --- .../core/tests/workflow/test_function_executor_future.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/packages/core/tests/workflow/test_function_executor_future.py b/python/packages/core/tests/workflow/test_function_executor_future.py index 3926536c02..a4a15aeba0 100644 --- a/python/packages/core/tests/workflow/test_function_executor_future.py +++ b/python/packages/core/tests/workflow/test_function_executor_future.py @@ -30,9 +30,7 @@ def test_executor_decorator_future_annotations_complex(self): """Test @executor decorator works with complex stringified annotations.""" @executor - async def process_complex( - data: dict[str, Any], ctx: WorkflowContext[list[str]] - ) -> None: + async def process_complex(data: dict[str, Any], ctx: WorkflowContext[list[str]]) -> None: await ctx.send_message(["done"]) assert isinstance(process_complex, FunctionExecutor)