Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion python/flink_agents/plan/actions/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion python/flink_agents/plan/agent_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]


Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning fires for @action-decorated methods here, but actions registered imperatively via add_action(name, events, func) flow through the agent.actions loop below (around line 311) and skip this check — so a plain Python callable with -> str passed to add_action hits the same silent-discard trap with no warning. Issue #834 scopes this to decorated methods, so this may be deliberate — is covering the add_action path worth a follow-up, or intentionally out of scope?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can move this validation to the initialize function of Action, after the self.exec.check_signature(Event, RunnerContext). Thus, we no longer need to handle different action construction paths, such as @action and add_action.

exec_ = (
_to_plan_function(target)
if target is not None
Expand Down
30 changes: 30 additions & 0 deletions python/flink_agents/plan/tests/test_agent_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# limitations under the License.
#################################################################################
import json
import logging
from pathlib import Path
from typing import Any, ClassVar, Dict, List, Sequence

Expand Down Expand Up @@ -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
Expand Down
Loading