Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,13 @@ class SendActivityExecutor(DeclarativeActionExecutor):
"""Executor for the SendActivity action.

Sends a text message or activity as workflow output.
Authored text starting with ``=`` is evaluated and its result is emitted as data.
Other authored text supports ``{Variable.Path}`` template interpolation.

Expression results are not interpolated again. To migrate text that relied on
a second pass, author the template directly (``Hello, {Local.name}!``) or build
the final text in the expression (``="Hello, " & Local.name & "!"``).
Both forms work as a string activity or as a mapping's ``text`` field.
"""

@handler
Expand All @@ -244,11 +251,7 @@ async def handle_action(
text = activity

if isinstance(text, str):
# First evaluate any =expression syntax
text = state.eval_if_expression(text)
# Then interpolate any {Variable.Path} template syntax
if isinstance(text, str):
text = state.interpolate_string(text)
text = state.eval_if_expression(text) if text.startswith("=") else state.interpolate_string(text)
Comment thread
jpalvarezl marked this conversation as resolved.
Comment thread
jpalvarezl marked this conversation as resolved.

# Yield the text as workflow output
if text:
Expand Down
41 changes: 37 additions & 4 deletions python/packages/declarative/tests/test_graph_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,20 @@ async def test_clear_all_variables_executor(self, mock_context, mock_state):
assert state.get("Local.a") is None
assert state.get("Local.b") is None

async def test_send_activity_with_dict_activity(self, mock_context, mock_state):
"""Test SendActivityExecutor with dict activity containing text field."""
@_requires_powerfx
@pytest.mark.parametrize(
"activity",
[
"Hello, {Local.name}!",
{"text": "Hello, {Local.name}!"},
'="Hello, " & Local.name & "!"',
{"text": '="Hello, " & Local.name & "!"'},
],
)
async def test_send_activity_with_authored_greeting(
self, mock_context: MagicMock, mock_state: MagicMock, activity: str | dict[str, str]
) -> None:
"""Authored templates and explicit expressions support the same greeting."""
from agent_framework_declarative._workflows._executors_basic import (
SendActivityExecutor,
)
Expand All @@ -568,12 +580,12 @@ async def test_send_activity_with_dict_activity(self, mock_context, mock_state):

action_def = {
"kind": "SendActivity",
"activity": {"text": "Hello, {Local.name}!"},
"activity": activity,
}
executor = SendActivityExecutor(action_def)
await executor.handle_action(ActionTrigger(), mock_context)

mock_context.yield_output.assert_called_once_with("Hello, Alice!")
mock_context.yield_output.assert_awaited_once_with("Hello, Alice!")

async def test_send_activity_with_string_activity(self, mock_context, mock_state):
"""Test SendActivityExecutor with string activity."""
Expand Down Expand Up @@ -613,6 +625,27 @@ async def test_send_activity_with_expression(self, mock_context, mock_state):

mock_context.yield_output.assert_called_once_with("Dynamic message")

@_requires_powerfx
@pytest.mark.parametrize("activity", ["=Local.msg", {"text": "=Local.msg"}])
async def test_send_activity_preserves_expression_result(
self, mock_context: MagicMock, mock_state: MagicMock, activity: str | dict[str, str]
) -> None:
"""Expression results are output data, not authored templates."""
from agent_framework_declarative._workflows._executors_basic import (
SendActivityExecutor,
)

state = DeclarativeWorkflowState(mock_state)
state.initialize()
message = "Keep {Local.marker} as text."
state.set("Local.msg", message)
state.set("Local.marker", "fixture value")

executor = SendActivityExecutor({"kind": "SendActivity", "activity": activity})
await executor.handle_action(ActionTrigger(), mock_context)

mock_context.yield_output.assert_awaited_once_with(message)


# ---------------------------------------------------------------------------
# Agent Executors Tests - Covering _executors_agents.py gaps
Expand Down
Loading