Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import locale
import logging
import math
import os
import re
import sys
Expand Down Expand Up @@ -658,6 +659,37 @@ def _eval_custom_function(self, formula: str) -> Any | None:
# Reuse the helper method for consistent text extraction
return self._eval_and_replace_message_text(inner_expr)

# Int(expr) - Python-side fallback for the PowerFx Int() function.
# Uses math.floor() to preserve Power Fx floor semantics (Int(-4.2) → -5,
# not -4). Only handles a single balanced call; compound expressions such
# as =Int(a) + Int(b) are left to the PowerFx engine.
match = re.match(r"Int\((.+)\)$", formula.strip())
Comment thread
amit12cool marked this conversation as resolved.
if match:
inner_expr = match.group(1).strip()
# Guard against greedy matches: if inner_expr contains a ')' that
# pushes paren depth negative, this is a compound expression.
depth = 0
single_call = True
for ch in inner_expr:
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth < 0:
single_call = False
break
if single_call:
raw = self.eval(f"={inner_expr}")
if raw in (None, ""):
return 0
try:
val = float(str(raw))
except (ValueError, TypeError) as exc:
raise ValueError(f"Int() cannot convert {raw!r} to a number") from exc
if not math.isfinite(val):
raise ValueError(f"Int() received a non-finite value: {raw!r}")
return math.floor(val)

return None

def _preprocess_custom_functions(self, formula: str, temp_writes: list[tuple[str, Any]]) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ async def handle_action(
self,
trigger: dict[str, Any]
| str
| Message
| list[Message]
| ActionTrigger
| ActionComplete
Expand All @@ -408,6 +409,10 @@ async def handle_action(
ctx: WorkflowContext[ActionComplete],
) -> None:
"""Simply pass through to continue the workflow."""
# Normalize a single Message to list[Message] so _ensure_state_initialized
# can extract the user text via the list[Message] path.
if isinstance(trigger, Message):
trigger = [trigger]
Comment thread
amit12cool marked this conversation as resolved.
await self._ensure_state_initialized(ctx, trigger)
await ctx.send_message(ActionComplete())

Expand Down
41 changes: 41 additions & 0 deletions python/packages/declarative/tests/test_workflow_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest.mock import patch

import pytest
from agent_framework import Content, Message

from agent_framework_declarative._feature_usage import FeatureIndex
from agent_framework_declarative._workflows._errors import DeclarativeWorkflowError
Expand Down Expand Up @@ -338,6 +339,46 @@ async def test_as_agent_continuation_preserves_prior_state(self):
f"System.LastMessageText not refreshed on turn 2: {post_state.get('System')!r}"
)

@pytest.mark.asyncio
async def test_entry_join_executor_single_message_trigger(self):
"""Regression test: JoinExecutor must correctly initialize state when passed
a single Message (not list[Message]) as the trigger.

Without the normalization fix in JoinExecutor, a lone Message would bypass
the list[Message] path in _ensure_state_initialized, leaving
System.LastMessage.Text empty. This caused =Int(System.LastMessage.Text) to
evaluate as 0 and age-based conditions to always resolve to the wrong branch.
"""
factory = WorkflowFactory()
workflow = factory.create_workflow_from_yaml("""
name: single-message-trigger-test
actions:
- kind: SetValue
path: Local.age
value: =Int(System.LastMessage.Text)
- kind: If
condition: =Local.age < 13
then:
- kind: SendActivity
activity:
text: child
else:
- kind: SendActivity
activity:
text: adult
""")

msg = Message(role="user", contents=[Content.from_text("25")])
result = await workflow.run(msg)
outputs = result.get_outputs()
assert any("adult" in str(o) for o in outputs), (
f"Expected 'adult' for age=25 passed as single Message, got: {outputs}. "
"JoinExecutor may not be normalizing single Message to list[Message]."
)
assert not any("child" in str(o) for o in outputs), (
f"Did not expect 'child' for age=25 passed as single Message, got: {outputs}"
)


class TestWorkflowFactoryAgentRegistration:
"""Tests for agent registration."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Demonstrates conditional branching based on age input using YAML-defined workflow.
"""

import logging
from pathlib import Path

from agent_framework.declarative import WorkflowFactory
Expand All @@ -18,6 +19,7 @@

def main():
"""Run the declarative workflow with DevUI."""
logging.basicConfig(level=logging.INFO, format="%(message)s")
serve(entities=[workflow], auto_open=True)


Expand Down
29 changes: 12 additions & 17 deletions python/samples/02-agents/devui/workflow_declarative/workflow.yaml
Original file line number Diff line number Diff line change
@@ -1,52 +1,47 @@
name: conditional-workflow
description: Demonstrates conditional branching based on user input

inputs:
age:
type: integer
description: The user's age in years

actions:
- kind: SetValue
id: get_age
displayName: Get user age
path: turn.age
value: =inputs.age
path: Local.age
value: =Int(System.LastMessage.Text)

- kind: If
id: check_age
displayName: Check age category
condition: =turn.age < 13
condition: =Local.age < 13
then:
- kind: SetValue
path: turn.category
path: Local.category
value: child
- kind: SendActivity
activity:
text: "Welcome, young one! Here are some fun activities for kids."
else:
- kind: If
condition: =turn.age < 20
condition: =Local.age < 20
then:
- kind: SetValue
path: turn.category
path: Local.category
value: teenager
- kind: SendActivity
activity:
text: "Hey there! Check out these cool things for teens."
else:
- kind: If
condition: =turn.age < 65
condition: =Local.age < 65
then:
- kind: SetValue
path: turn.category
path: Local.category
value: adult
- kind: SendActivity
activity:
text: "Welcome! Here are our professional services."
else:
- kind: SetValue
path: turn.category
path: Local.category
value: senior
- kind: SendActivity
activity:
Expand All @@ -56,9 +51,9 @@ actions:
id: summary
displayName: Send category summary
activity:
text: '=Concat("You have been categorized as: ", turn.category)'
text: '=Concat("You have been categorized as: ", Local.category)'

- kind: SetValue
id: set_output
path: workflow.outputs.category
value: =turn.category
path: Workflow.Outputs.category
value: =Local.category
Loading