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
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,29 @@ 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) - convert to integer (Python-side fallback so the function
# works even when PowerFx is unavailable or returns an ErrorValue for
# non-numeric input). Returns 0 for blank/unconvertible values, matching
# PowerFx's Blank-as-zero coercion behaviour.
match = re.match(r"Int\((.+)\)$", formula.strip())
if match:
inner_expr = match.group(1).strip()
raw = self.eval(f"={inner_expr}")
try:
return int(float(str(raw))) if raw not in (None, "") else 0
except (ValueError, TypeError):
return 0

# Float(expr) / Value(expr) - convert to float (Python-side fallback).
match = re.match(r"(?:Float|Value)\((.+)\)$", formula.strip())
if match:
inner_expr = match.group(1).strip()
raw = self.eval(f"={inner_expr}")
try:
return float(str(raw)) if raw not in (None, "") else 0.0
except (ValueError, TypeError):
return 0.0

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 @@

import pytest

from agent_framework import Content, Message
from agent_framework_declarative._workflows._errors import DeclarativeWorkflowError
from agent_framework_declarative._workflows._factory import WorkflowFactory

Expand Down Expand Up @@ -318,6 +319,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