diff --git a/python/flink_agents/plan/actions/action.py b/python/flink_agents/plan/actions/action.py index 96f2f5f0a..289cd2409 100644 --- a/python/flink_agents/plan/actions/action.py +++ b/python/flink_agents/plan/actions/action.py @@ -47,7 +47,6 @@ class Action(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) name: str - # TODO: Raise a warning when the action has a return value, as it will be ignored. exec: PythonFunction | JavaFunction listen_event_types: List[str] config: Dict[str, Any] | None = None diff --git a/python/flink_agents/plan/agent_plan.py b/python/flink_agents/plan/agent_plan.py index 24aca0caf..fdbd8c502 100644 --- a/python/flink_agents/plan/agent_plan.py +++ b/python/flink_agents/plan/agent_plan.py @@ -15,7 +15,9 @@ # See the License for the specific language governing permissions and # limitations under the License. ################################################################################# -from typing import TYPE_CHECKING, Any, Dict, List, cast +import inspect +import logging +from typing import TYPE_CHECKING, Any, Callable, Dict, List, cast from pydantic import BaseModel, field_serializer, model_validator @@ -56,6 +58,8 @@ ) from flink_agents.integrations.mcp.mcp import MCPServer +logger = logging.getLogger(__name__) + BUILT_IN_ACTIONS = [CHAT_MODEL_ACTION, TOOL_CALL_ACTION, CONTEXT_RETRIEVAL_ACTION] @@ -248,6 +252,16 @@ def _action_marker(value: Any) -> tuple | None: return inner, marker._listen_events, getattr(marker, "_target", None) +def _warn_if_action_returns_value(name: str, func: Callable) -> None: + """Warn if an action declares a non-None return type; its value is ignored.""" + return_annotation = inspect.signature(func).return_annotation + if return_annotation not in (inspect.Signature.empty, None, type(None), "None"): + logger.warning( + f"Action '{name}' declares return type '{return_annotation}', but action " + f"return values are ignored; use ctx.send_event(...) instead." + ) + + def _get_actions(agent: Agent) -> List[Action]: """Extract all registered agent actions from an agent. @@ -280,6 +294,8 @@ def _get_actions(agent: Agent) -> List[Action]: if marker is None: continue inner, listen_events, target = marker + if target is None: + _warn_if_action_returns_value(name, inner) exec_ = ( _to_plan_function(target) if target is not None diff --git a/python/flink_agents/plan/tests/test_agent_plan.py b/python/flink_agents/plan/tests/test_agent_plan.py index 9d68f8c1b..8b1e035d7 100644 --- a/python/flink_agents/plan/tests/test_agent_plan.py +++ b/python/flink_agents/plan/tests/test_agent_plan.py @@ -16,6 +16,7 @@ # limitations under the License. ################################################################################# import json +import logging from pathlib import Path from typing import Any, ClassVar, Dict, List, Sequence @@ -72,6 +73,35 @@ def test_from_agent(): assert action.listen_event_types == [InputEvent.EVENT_TYPE] +class AgentWithReturningAction(Agent): + @action(InputEvent.EVENT_TYPE) + @staticmethod + def returns_value(event: Event, ctx: RunnerContext) -> str: + return "result" + + +def test_warns_when_action_returns_value( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + AgentPlan.from_agent(AgentWithReturningAction(), AgentConfiguration()) + assert any( + "returns_value" in record.getMessage() + and "ignored" in record.getMessage().lower() + for record in caplog.records + ) + + +def test_no_warning_for_none_returning_action( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING): + AgentPlan.from_agent(AgentForTest(), AgentConfiguration()) + assert not any( + "ignored" in record.getMessage().lower() for record in caplog.records + ) + + class InvalidAgent(Agent): @action(InputEvent.EVENT_TYPE) @staticmethod