mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Refactor SharedState to State with sync methods and superstep caching (#3667)
* Refactor SharedState to State with sync methods and superstep caching * Fixes * Address PR feedback * Remove dead links * Fix lab test import
This commit is contained in:
committed by
GitHub
Unverified
parent
4e25917644
commit
10afb86213
+63
-64
@@ -3,7 +3,7 @@
|
||||
"""Base classes for graph-based declarative workflow executors.
|
||||
|
||||
This module provides:
|
||||
- DeclarativeWorkflowState: Manages workflow variables via SharedState
|
||||
- DeclarativeWorkflowState: Manages workflow variables via State
|
||||
- DeclarativeActionExecutor: Base class for action executors
|
||||
- Message types for inter-executor communication
|
||||
|
||||
@@ -34,7 +34,7 @@ from agent_framework._workflows import (
|
||||
Executor,
|
||||
WorkflowContext,
|
||||
)
|
||||
from agent_framework._workflows._shared_state import SharedState
|
||||
from agent_framework._workflows._state import State
|
||||
from powerfx import Engine
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
@@ -61,10 +61,10 @@ class ConversationData(TypedDict):
|
||||
|
||||
|
||||
class DeclarativeStateData(TypedDict, total=False):
|
||||
"""Structure for the declarative workflow state stored in SharedState.
|
||||
"""Structure for the declarative workflow state stored in State.
|
||||
|
||||
This TypedDict defines the schema for workflow variables stored
|
||||
under the DECLARATIVE_STATE_KEY in SharedState.
|
||||
under the DECLARATIVE_STATE_KEY in State.
|
||||
|
||||
Variable Scopes (matching .NET naming conventions):
|
||||
Inputs: Initial workflow inputs (read-only after initialization).
|
||||
@@ -87,7 +87,7 @@ class DeclarativeStateData(TypedDict, total=False):
|
||||
_declarative_loop_state: dict[str, Any]
|
||||
|
||||
|
||||
# Key used in SharedState to store declarative workflow variables
|
||||
# Key used in State to store declarative workflow variables
|
||||
DECLARATIVE_STATE_KEY = "_declarative_workflow_state"
|
||||
|
||||
|
||||
@@ -126,10 +126,10 @@ def _make_powerfx_safe(value: Any) -> Any:
|
||||
|
||||
|
||||
class DeclarativeWorkflowState:
|
||||
"""Manages workflow variables stored in SharedState.
|
||||
"""Manages workflow variables stored in State.
|
||||
|
||||
This class provides the same interface as the interpreter-based WorkflowState
|
||||
but stores all data in SharedState for checkpointing support.
|
||||
but stores all data in State for checkpointing support.
|
||||
|
||||
The state is organized into namespaces (matching .NET naming conventions):
|
||||
- Workflow.Inputs: Initial inputs (read-only)
|
||||
@@ -140,15 +140,15 @@ class DeclarativeWorkflowState:
|
||||
- Conversation: Conversation history
|
||||
"""
|
||||
|
||||
def __init__(self, shared_state: SharedState):
|
||||
"""Initialize with a SharedState instance.
|
||||
def __init__(self, state: State):
|
||||
"""Initialize with a State instance.
|
||||
|
||||
Args:
|
||||
shared_state: The workflow's shared state for persistence
|
||||
state: The workflow's state for persistence
|
||||
"""
|
||||
self._shared_state = shared_state
|
||||
self._state = state
|
||||
|
||||
async def initialize(self, inputs: "Mapping[str, Any] | None" = None) -> None:
|
||||
def initialize(self, inputs: "Mapping[str, Any] | None" = None) -> None:
|
||||
"""Initialize the declarative state with inputs.
|
||||
|
||||
Args:
|
||||
@@ -168,23 +168,22 @@ class DeclarativeWorkflowState:
|
||||
"Conversation": {"messages": [], "history": []},
|
||||
"Custom": {},
|
||||
}
|
||||
await self._shared_state.set(DECLARATIVE_STATE_KEY, state_data)
|
||||
self._state.set(DECLARATIVE_STATE_KEY, state_data)
|
||||
|
||||
async def get_state_data(self) -> DeclarativeStateData:
|
||||
"""Get the full state data dict from shared state."""
|
||||
try:
|
||||
result: DeclarativeStateData = await self._shared_state.get(DECLARATIVE_STATE_KEY)
|
||||
return result
|
||||
except KeyError:
|
||||
def get_state_data(self) -> DeclarativeStateData:
|
||||
"""Get the full state data dict from state."""
|
||||
result = self._state.get(DECLARATIVE_STATE_KEY)
|
||||
if result is None:
|
||||
# Initialize if not present
|
||||
await self.initialize()
|
||||
return cast(DeclarativeStateData, await self._shared_state.get(DECLARATIVE_STATE_KEY))
|
||||
self.initialize()
|
||||
result = self._state.get(DECLARATIVE_STATE_KEY)
|
||||
return cast(DeclarativeStateData, result)
|
||||
|
||||
async def set_state_data(self, data: DeclarativeStateData) -> None:
|
||||
"""Set the full state data dict in shared state."""
|
||||
await self._shared_state.set(DECLARATIVE_STATE_KEY, data)
|
||||
def set_state_data(self, data: DeclarativeStateData) -> None:
|
||||
"""Set the full state data dict in state."""
|
||||
self._state.set(DECLARATIVE_STATE_KEY, data)
|
||||
|
||||
async def get(self, path: str, default: Any = None) -> Any:
|
||||
def get(self, path: str, default: Any = None) -> Any:
|
||||
"""Get a value from the state using a dot-notated path.
|
||||
|
||||
Args:
|
||||
@@ -194,7 +193,7 @@ class DeclarativeWorkflowState:
|
||||
Returns:
|
||||
The value at the path, or default if not found
|
||||
"""
|
||||
state_data = await self.get_state_data()
|
||||
state_data = self.get_state_data()
|
||||
parts = path.split(".")
|
||||
if not parts:
|
||||
return default
|
||||
@@ -240,7 +239,7 @@ class DeclarativeWorkflowState:
|
||||
|
||||
return obj # type: ignore[return-value]
|
||||
|
||||
async def set(self, path: str, value: Any) -> None:
|
||||
def set(self, path: str, value: Any) -> None:
|
||||
"""Set a value in the state using a dot-notated path.
|
||||
|
||||
Args:
|
||||
@@ -250,7 +249,7 @@ class DeclarativeWorkflowState:
|
||||
Raises:
|
||||
ValueError: If attempting to set Workflow.Inputs (which is read-only)
|
||||
"""
|
||||
state_data = await self.get_state_data()
|
||||
state_data = self.get_state_data()
|
||||
parts = path.split(".")
|
||||
if not parts:
|
||||
return
|
||||
@@ -296,9 +295,9 @@ class DeclarativeWorkflowState:
|
||||
|
||||
# Set the final value
|
||||
target[remaining[-1]] = value
|
||||
await self.set_state_data(state_data)
|
||||
self.set_state_data(state_data)
|
||||
|
||||
async def append(self, path: str, value: Any) -> None:
|
||||
def append(self, path: str, value: Any) -> None:
|
||||
"""Append a value to a list at the specified path.
|
||||
|
||||
If the path doesn't exist, creates a new list with the value.
|
||||
@@ -310,17 +309,17 @@ class DeclarativeWorkflowState:
|
||||
path: Dot-notated path to a list
|
||||
value: The value to append
|
||||
"""
|
||||
existing = await self.get(path)
|
||||
existing = self.get(path)
|
||||
if existing is None:
|
||||
await self.set(path, [value])
|
||||
self.set(path, [value])
|
||||
elif isinstance(existing, list):
|
||||
existing_list: list[Any] = list(existing) # type: ignore[arg-type]
|
||||
existing_list.append(value)
|
||||
await self.set(path, existing_list)
|
||||
self.set(path, existing_list)
|
||||
else:
|
||||
raise ValueError(f"Cannot append to non-list at path '{path}'")
|
||||
|
||||
async def eval(self, expression: str) -> Any:
|
||||
def eval(self, expression: str) -> Any:
|
||||
"""Evaluate a PowerFx expression with the current state.
|
||||
|
||||
Expressions starting with '=' are evaluated as PowerFx.
|
||||
@@ -354,16 +353,16 @@ class DeclarativeWorkflowState:
|
||||
|
||||
# Handle custom functions not supported by PowerFx
|
||||
# First check if the entire formula is a custom function
|
||||
result = await self._eval_custom_function(formula)
|
||||
result = self._eval_custom_function(formula)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
# Pre-process nested custom functions (e.g., Upper(MessageText(...)))
|
||||
# Replace them with their evaluated results before sending to PowerFx
|
||||
formula = await self._preprocess_custom_functions(formula)
|
||||
formula = self._preprocess_custom_functions(formula)
|
||||
|
||||
engine = Engine()
|
||||
symbols = await self._to_powerfx_symbols()
|
||||
symbols = self._to_powerfx_symbols()
|
||||
try:
|
||||
return engine.eval(formula, symbols=symbols)
|
||||
except ValueError as e:
|
||||
@@ -375,7 +374,7 @@ class DeclarativeWorkflowState:
|
||||
return None
|
||||
raise
|
||||
|
||||
async def _eval_custom_function(self, formula: str) -> Any | None:
|
||||
def _eval_custom_function(self, formula: str) -> Any | None:
|
||||
"""Handle custom functions not supported by the Python PowerFx library.
|
||||
|
||||
The standard PowerFx library supports these functions but the Python wrapper
|
||||
@@ -404,7 +403,7 @@ class DeclarativeWorkflowState:
|
||||
evaluated_args.append(arg[1:-1])
|
||||
else:
|
||||
# Variable reference - evaluate it
|
||||
result = await self.eval(f"={arg}")
|
||||
result = self.eval(f"={arg}")
|
||||
evaluated_args.append(str(result) if result is not None else "")
|
||||
return "".join(evaluated_args)
|
||||
|
||||
@@ -413,14 +412,14 @@ class DeclarativeWorkflowState:
|
||||
if match:
|
||||
inner_expr = match.group(1).strip()
|
||||
# Evaluate the inner expression
|
||||
text = await self.eval(f"={inner_expr}")
|
||||
text = self.eval(f"={inner_expr}")
|
||||
return {"role": "user", "text": str(text) if text else ""}
|
||||
|
||||
# AgentMessage(expr) - creates an assistant message dict
|
||||
match = re.match(r"AgentMessage\((.+)\)$", formula.strip())
|
||||
if match:
|
||||
inner_expr = match.group(1).strip()
|
||||
text = await self.eval(f"={inner_expr}")
|
||||
text = self.eval(f"={inner_expr}")
|
||||
return {"role": "assistant", "text": str(text) if text else ""}
|
||||
|
||||
# MessageText(expr) - extracts text from the last message
|
||||
@@ -428,11 +427,11 @@ class DeclarativeWorkflowState:
|
||||
if match:
|
||||
inner_expr = match.group(1).strip()
|
||||
# Reuse the helper method for consistent text extraction
|
||||
return await self._eval_and_replace_message_text(inner_expr)
|
||||
return self._eval_and_replace_message_text(inner_expr)
|
||||
|
||||
return None
|
||||
|
||||
async def _preprocess_custom_functions(self, formula: str) -> str:
|
||||
def _preprocess_custom_functions(self, formula: str) -> str:
|
||||
"""Pre-process custom functions nested inside other PowerFx functions.
|
||||
|
||||
Custom functions like MessageText() are not supported by the PowerFx engine.
|
||||
@@ -509,7 +508,7 @@ class DeclarativeWorkflowState:
|
||||
inner_expr = formula[paren_start + 1 : end - 1]
|
||||
|
||||
# Evaluate and get replacement
|
||||
replacement = await handler(inner_expr)
|
||||
replacement = handler(inner_expr)
|
||||
|
||||
# Replace in formula
|
||||
if isinstance(replacement, str):
|
||||
@@ -517,7 +516,7 @@ class DeclarativeWorkflowState:
|
||||
# Store long strings in a temp variable to avoid PowerFx expression limit
|
||||
temp_var_name = f"_TempMessageText{temp_var_counter}"
|
||||
temp_var_counter += 1
|
||||
await self.set(f"Local.{temp_var_name}", replacement)
|
||||
self.set(f"Local.{temp_var_name}", replacement)
|
||||
replacement_str = f"Local.{temp_var_name}"
|
||||
logger.debug(
|
||||
f"Stored long MessageText result ({len(replacement)} chars) "
|
||||
@@ -534,7 +533,7 @@ class DeclarativeWorkflowState:
|
||||
|
||||
return formula
|
||||
|
||||
async def _eval_and_replace_message_text(self, inner_expr: str) -> str:
|
||||
def _eval_and_replace_message_text(self, inner_expr: str) -> str:
|
||||
"""Evaluate MessageText() and return the text result.
|
||||
|
||||
Args:
|
||||
@@ -543,7 +542,7 @@ class DeclarativeWorkflowState:
|
||||
Returns:
|
||||
The extracted text from the messages
|
||||
"""
|
||||
messages: Any = await self.eval(f"={inner_expr}")
|
||||
messages: Any = self.eval(f"={inner_expr}")
|
||||
if isinstance(messages, list) and messages:
|
||||
last_msg: Any = messages[-1]
|
||||
if isinstance(last_msg, dict):
|
||||
@@ -603,13 +602,13 @@ class DeclarativeWorkflowState:
|
||||
|
||||
return args
|
||||
|
||||
async def _to_powerfx_symbols(self) -> dict[str, Any]:
|
||||
def _to_powerfx_symbols(self) -> dict[str, Any]:
|
||||
"""Convert the current state to a PowerFx symbols dictionary.
|
||||
|
||||
Uses .NET-style PascalCase names (System, Local, Workflow) matching
|
||||
the .NET declarative workflow implementation.
|
||||
"""
|
||||
state_data = await self.get_state_data()
|
||||
state_data = self.get_state_data()
|
||||
local_data = state_data.get("Local", {})
|
||||
agent_data = state_data.get("Agent", {})
|
||||
conversation_data = state_data.get("Conversation", {})
|
||||
@@ -642,19 +641,19 @@ class DeclarativeWorkflowState:
|
||||
result = _make_powerfx_safe(symbols)
|
||||
return cast(dict[str, Any], result)
|
||||
|
||||
async def eval_if_expression(self, value: Any) -> Any:
|
||||
def eval_if_expression(self, value: Any) -> Any:
|
||||
"""Evaluate a value if it's a PowerFx expression, otherwise return as-is."""
|
||||
if isinstance(value, str):
|
||||
return await self.eval(value)
|
||||
return self.eval(value)
|
||||
if isinstance(value, dict):
|
||||
value_dict: dict[str, Any] = dict(value) # type: ignore[arg-type]
|
||||
return {k: await self.eval_if_expression(v) for k, v in value_dict.items()}
|
||||
return {k: self.eval_if_expression(v) for k, v in value_dict.items()}
|
||||
if isinstance(value, list):
|
||||
value_list: list[Any] = list(value) # type: ignore[arg-type]
|
||||
return [await self.eval_if_expression(item) for item in value_list]
|
||||
return [self.eval_if_expression(item) for item in value_list]
|
||||
return value
|
||||
|
||||
async def interpolate_string(self, text: str) -> str:
|
||||
def interpolate_string(self, text: str) -> str:
|
||||
"""Interpolate {Variable.Path} references in a string.
|
||||
|
||||
This handles template-style variable substitution like:
|
||||
@@ -669,18 +668,18 @@ class DeclarativeWorkflowState:
|
||||
"""
|
||||
import re
|
||||
|
||||
async def replace_var(match: re.Match[str]) -> str:
|
||||
def replace_var(match: re.Match[str]) -> str:
|
||||
var_path: str = match.group(1)
|
||||
value = await self.get(var_path)
|
||||
value = self.get(var_path)
|
||||
return str(value) if value is not None else ""
|
||||
|
||||
# Match {Variable.Path} patterns
|
||||
pattern = r"\{([A-Za-z][A-Za-z0-9_.]*)\}"
|
||||
|
||||
# re.sub doesn't support async, so we need to do it manually
|
||||
# Replace all matches
|
||||
result = text
|
||||
for match in re.finditer(pattern, text):
|
||||
replacement = await replace_var(match)
|
||||
replacement = replace_var(match)
|
||||
result = result.replace(match.group(0), replacement, 1)
|
||||
|
||||
return result
|
||||
@@ -802,9 +801,9 @@ class DeclarativeActionExecutor(Executor):
|
||||
"""Get the display name for logging."""
|
||||
return self._action_def.get("displayName")
|
||||
|
||||
def _get_state(self, shared_state: SharedState) -> DeclarativeWorkflowState:
|
||||
def _get_state(self, state: State) -> DeclarativeWorkflowState:
|
||||
"""Get the declarative workflow state wrapper."""
|
||||
return DeclarativeWorkflowState(shared_state)
|
||||
return DeclarativeWorkflowState(state)
|
||||
|
||||
async def _ensure_state_initialized(
|
||||
self,
|
||||
@@ -826,18 +825,18 @@ class DeclarativeActionExecutor(Executor):
|
||||
Returns:
|
||||
The initialized DeclarativeWorkflowState
|
||||
"""
|
||||
state = self._get_state(ctx.shared_state)
|
||||
state = self._get_state(ctx.state)
|
||||
|
||||
if isinstance(trigger, dict):
|
||||
# Structured inputs - use directly
|
||||
await state.initialize(trigger) # type: ignore
|
||||
state.initialize(trigger) # type: ignore
|
||||
elif isinstance(trigger, str):
|
||||
# String input - wrap in dict
|
||||
await state.initialize({"input": trigger})
|
||||
state.initialize({"input": trigger})
|
||||
elif not isinstance(
|
||||
trigger, (ActionTrigger, ActionComplete, ConditionResult, LoopIterationResult, LoopControl)
|
||||
):
|
||||
# Any other type - convert to string like .NET's DefaultTransform
|
||||
await state.initialize({"input": str(trigger)})
|
||||
state.initialize({"input": str(trigger)})
|
||||
|
||||
return state
|
||||
|
||||
+49
-50
@@ -348,7 +348,7 @@ class AgentExternalInputResponse:
|
||||
class ExternalLoopState:
|
||||
"""State saved for external loop resumption.
|
||||
|
||||
Stored in shared_state to allow the response_handler to
|
||||
Stored in workflow state to allow the response_handler to
|
||||
continue the loop with the same configuration.
|
||||
"""
|
||||
|
||||
@@ -534,7 +534,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
return "Conversation.messages"
|
||||
|
||||
# Evaluate the conversation ID expression
|
||||
evaluated_id = await state.eval_if_expression(conversation_id_expr)
|
||||
evaluated_id = state.eval_if_expression(conversation_id_expr)
|
||||
if not evaluated_id:
|
||||
return "Conversation.messages"
|
||||
|
||||
@@ -555,11 +555,11 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
# Evaluate arguments
|
||||
evaluated_args: dict[str, Any] = {}
|
||||
for key, value in arguments.items():
|
||||
evaluated_args[key] = await state.eval_if_expression(value)
|
||||
evaluated_args[key] = state.eval_if_expression(value)
|
||||
|
||||
# Evaluate messages/input
|
||||
if messages_expr:
|
||||
evaluated_input: Any = await state.eval_if_expression(messages_expr)
|
||||
evaluated_input: Any = state.eval_if_expression(messages_expr)
|
||||
if isinstance(evaluated_input, str):
|
||||
return evaluated_input
|
||||
if isinstance(evaluated_input, list) and evaluated_input:
|
||||
@@ -581,17 +581,17 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
# 1. Local.input / Local.userInput (explicit turn state)
|
||||
# 2. System.LastMessage.Text (previous agent's response)
|
||||
# 3. Workflow.Inputs (first agent gets workflow inputs)
|
||||
input_text: str = str(await state.get("Local.input") or await state.get("Local.userInput") or "")
|
||||
input_text: str = str(state.get("Local.input") or state.get("Local.userInput") or "")
|
||||
if not input_text:
|
||||
# Try System.LastMessage.Text (used by external loop and agent chaining)
|
||||
last_message: Any = await state.get("System.LastMessage")
|
||||
last_message: Any = state.get("System.LastMessage")
|
||||
if isinstance(last_message, dict):
|
||||
last_msg_dict = cast(dict[str, Any], last_message)
|
||||
text_val: Any = last_msg_dict.get("Text", "")
|
||||
input_text = str(text_val) if text_val else ""
|
||||
if not input_text:
|
||||
# Fall back to workflow inputs (for first agent in chain)
|
||||
inputs: Any = await state.get("Workflow.Inputs")
|
||||
inputs: Any = state.get("Workflow.Inputs")
|
||||
if isinstance(inputs, dict):
|
||||
inputs_dict = cast(dict[str, Any], inputs)
|
||||
# If single input, use its value directly
|
||||
@@ -642,12 +642,12 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
|
||||
# Add user input to conversation history first (via state.append only)
|
||||
if input_text:
|
||||
user_message = ChatMessage("user", [input_text])
|
||||
await state.append(messages_path, user_message)
|
||||
user_message = ChatMessage(role="user", text=input_text)
|
||||
state.append(messages_path, user_message)
|
||||
|
||||
# Get conversation history from state AFTER adding user message
|
||||
# Note: We get a fresh copy to avoid mutation issues
|
||||
conversation_history: list[ChatMessage] = await state.get(messages_path) or []
|
||||
conversation_history: list[ChatMessage] = state.get(messages_path) or []
|
||||
|
||||
# Build messages list for agent (use history if available, otherwise just input)
|
||||
messages_for_agent: list[ChatMessage] | str = conversation_history if conversation_history else input_text
|
||||
@@ -704,32 +704,32 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
role,
|
||||
content_types,
|
||||
)
|
||||
await state.append(messages_path, msg)
|
||||
state.append(messages_path, msg)
|
||||
elif accumulated_response:
|
||||
# No messages returned, create a simple assistant message
|
||||
logger.debug(
|
||||
"Agent '%s': No messages in response, creating simple assistant message",
|
||||
agent_name,
|
||||
)
|
||||
assistant_message = ChatMessage("assistant", [accumulated_response])
|
||||
await state.append(messages_path, assistant_message)
|
||||
assistant_message = ChatMessage(role="assistant", text=accumulated_response)
|
||||
state.append(messages_path, assistant_message)
|
||||
|
||||
# Store results in state - support both schema formats:
|
||||
# - Graph mode: Agent.response, Agent.name
|
||||
# - Interpreter mode: Agent.text, Agent.messages, Agent.toolCalls
|
||||
await state.set("Agent.response", accumulated_response)
|
||||
await state.set("Agent.name", agent_name)
|
||||
await state.set("Agent.text", accumulated_response)
|
||||
await state.set("Agent.messages", all_messages if all_messages else [])
|
||||
await state.set("Agent.toolCalls", tool_calls if tool_calls else [])
|
||||
state.set("Agent.response", accumulated_response)
|
||||
state.set("Agent.name", agent_name)
|
||||
state.set("Agent.text", accumulated_response)
|
||||
state.set("Agent.messages", all_messages if all_messages else [])
|
||||
state.set("Agent.toolCalls", tool_calls if tool_calls else [])
|
||||
|
||||
# Store System.LastMessage for externalLoop.when condition evaluation
|
||||
await state.set("System.LastMessage", {"Text": accumulated_response})
|
||||
state.set("System.LastMessage", {"Text": accumulated_response})
|
||||
|
||||
# Store in output variables (.NET style)
|
||||
if messages_var:
|
||||
output_path = _normalize_variable_path(messages_var)
|
||||
await state.set(output_path, all_messages if all_messages else accumulated_response)
|
||||
state.set(output_path, all_messages if all_messages else accumulated_response)
|
||||
|
||||
if response_obj_var:
|
||||
output_path = _normalize_variable_path(response_obj_var)
|
||||
@@ -737,14 +737,14 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
try:
|
||||
parsed = _extract_json_from_response(accumulated_response) if accumulated_response else None
|
||||
logger.debug(f"InvokeAzureAgent: parsed responseObject for '{output_path}': type={type(parsed)}")
|
||||
await state.set(output_path, parsed)
|
||||
state.set(output_path, parsed)
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
logger.warning(f"InvokeAzureAgent: failed to parse JSON for '{output_path}': {e}, storing as string")
|
||||
await state.set(output_path, accumulated_response)
|
||||
state.set(output_path, accumulated_response)
|
||||
|
||||
# Store in result property (Python style)
|
||||
if result_property:
|
||||
await state.set(result_property, accumulated_response)
|
||||
state.set(result_property, accumulated_response)
|
||||
|
||||
return accumulated_response, all_messages, tool_calls
|
||||
|
||||
@@ -788,7 +788,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
agent: Any = self._agents.get(agent_name) if self._agents else None
|
||||
if agent is None:
|
||||
try:
|
||||
agent_registry: dict[str, Any] | None = await ctx.shared_state.get(AGENT_REGISTRY_KEY)
|
||||
agent_registry: dict[str, Any] | None = ctx.state.get(AGENT_REGISTRY_KEY)
|
||||
except KeyError:
|
||||
agent_registry = {}
|
||||
agent = agent_registry.get(agent_name) if agent_registry else None
|
||||
@@ -796,9 +796,9 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
if agent is None:
|
||||
error_msg = f"Agent '{agent_name}' not found in registry"
|
||||
logger.error(f"InvokeAzureAgent: {error_msg}")
|
||||
await state.set("Agent.error", error_msg)
|
||||
state.set("Agent.error", error_msg)
|
||||
if result_property:
|
||||
await state.set(result_property, {"error": error_msg})
|
||||
state.set(result_property, {"error": error_msg})
|
||||
raise AgentInvocationError(agent_name, "not found in registry")
|
||||
|
||||
iteration = 0
|
||||
@@ -820,14 +820,14 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
raise # Re-raise our own errors
|
||||
except Exception as e:
|
||||
logger.error(f"InvokeAzureAgent: error invoking agent '{agent_name}': {e}")
|
||||
await state.set("Agent.error", str(e))
|
||||
state.set("Agent.error", str(e))
|
||||
if result_property:
|
||||
await state.set(result_property, {"error": str(e)})
|
||||
state.set(result_property, {"error": str(e)})
|
||||
raise AgentInvocationError(agent_name, str(e)) from e
|
||||
|
||||
# Check external loop condition
|
||||
if external_loop_when:
|
||||
should_continue = await state.eval(external_loop_when)
|
||||
should_continue = state.eval(external_loop_when)
|
||||
should_continue = bool(should_continue) if should_continue is not None else False
|
||||
|
||||
logger.debug(
|
||||
@@ -848,7 +848,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
messages_path=messages_path,
|
||||
max_iterations=max_iterations,
|
||||
)
|
||||
await ctx.shared_state.set(EXTERNAL_LOOP_STATE_KEY, loop_state)
|
||||
ctx.state.set(EXTERNAL_LOOP_STATE_KEY, loop_state)
|
||||
|
||||
# Emit request for external input - workflow will yield here
|
||||
request = AgentExternalInputRequest(
|
||||
@@ -883,12 +883,11 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
"handle_external_input_response: resuming with user_input='%s'",
|
||||
response.user_input[:100] if response.user_input else None,
|
||||
)
|
||||
state = self._get_state(ctx.shared_state)
|
||||
state = self._get_state(ctx.state)
|
||||
|
||||
# Retrieve saved loop state
|
||||
try:
|
||||
loop_state: ExternalLoopState = await ctx.shared_state.get(EXTERNAL_LOOP_STATE_KEY)
|
||||
except KeyError:
|
||||
loop_state: ExternalLoopState | None = ctx.state.get(EXTERNAL_LOOP_STATE_KEY)
|
||||
if loop_state is None:
|
||||
logger.error("InvokeAzureAgent: external loop state not found, cannot resume")
|
||||
await ctx.send_message(ActionComplete())
|
||||
return
|
||||
@@ -910,12 +909,12 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
input_text = response.user_input
|
||||
|
||||
# Store the user input in state for condition evaluation
|
||||
await state.set("Local.userInput", input_text)
|
||||
await state.set("System.LastMessage", {"Text": input_text})
|
||||
state.set("Local.userInput", input_text)
|
||||
state.set("System.LastMessage", {"Text": input_text})
|
||||
|
||||
# Check if we should continue BEFORE invoking the agent
|
||||
# This matches .NET behavior where the condition checks the user's input
|
||||
should_continue = await state.eval(external_loop_when)
|
||||
should_continue = state.eval(external_loop_when)
|
||||
should_continue = bool(should_continue) if should_continue is not None else False
|
||||
|
||||
logger.debug(
|
||||
@@ -926,7 +925,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
if not should_continue:
|
||||
# User input caused loop to exit - clean up and complete
|
||||
with contextlib.suppress(KeyError):
|
||||
await ctx.shared_state.delete(EXTERNAL_LOOP_STATE_KEY)
|
||||
ctx.state.delete(EXTERNAL_LOOP_STATE_KEY)
|
||||
await ctx.send_message(ActionComplete())
|
||||
return
|
||||
|
||||
@@ -934,7 +933,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
agent: Any = self._agents.get(agent_name) if self._agents else None
|
||||
if agent is None:
|
||||
try:
|
||||
agent_registry: dict[str, Any] | None = await ctx.shared_state.get(AGENT_REGISTRY_KEY)
|
||||
agent_registry: dict[str, Any] | None = ctx.state.get(AGENT_REGISTRY_KEY)
|
||||
except KeyError:
|
||||
agent_registry = {}
|
||||
agent = agent_registry.get(agent_name) if agent_registry else None
|
||||
@@ -960,12 +959,12 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
raise # Re-raise our own errors
|
||||
except Exception as e:
|
||||
logger.error(f"InvokeAzureAgent: error invoking agent '{agent_name}' during loop: {e}")
|
||||
await state.set("Agent.error", str(e))
|
||||
state.set("Agent.error", str(e))
|
||||
raise AgentInvocationError(agent_name, str(e)) from e
|
||||
|
||||
# Re-evaluate the condition AFTER the agent responds
|
||||
# This is critical: the agent's response may have set NeedsTicket=true or IsResolved=true
|
||||
should_continue = await state.eval(external_loop_when)
|
||||
should_continue = state.eval(external_loop_when)
|
||||
should_continue = bool(should_continue) if should_continue is not None else False
|
||||
|
||||
logger.debug(
|
||||
@@ -980,7 +979,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
"(sending ActionComplete to continue workflow)"
|
||||
)
|
||||
with contextlib.suppress(KeyError):
|
||||
await ctx.shared_state.delete(EXTERNAL_LOOP_STATE_KEY)
|
||||
ctx.state.delete(EXTERNAL_LOOP_STATE_KEY)
|
||||
await ctx.send_message(ActionComplete())
|
||||
return
|
||||
|
||||
@@ -988,7 +987,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
if iteration < max_iterations:
|
||||
# Update loop state for next iteration
|
||||
loop_state.iteration = iteration + 1
|
||||
await ctx.shared_state.set(EXTERNAL_LOOP_STATE_KEY, loop_state)
|
||||
ctx.state.set(EXTERNAL_LOOP_STATE_KEY, loop_state)
|
||||
|
||||
# Emit another request for external input
|
||||
request = AgentExternalInputRequest(
|
||||
@@ -1007,7 +1006,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
|
||||
# Loop complete - clean up and send completion
|
||||
with contextlib.suppress(KeyError):
|
||||
await ctx.shared_state.delete(EXTERNAL_LOOP_STATE_KEY)
|
||||
ctx.state.delete(EXTERNAL_LOOP_STATE_KEY)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -1035,7 +1034,7 @@ class InvokeToolExecutor(DeclarativeActionExecutor):
|
||||
|
||||
# Get tools registry
|
||||
try:
|
||||
tool_registry: dict[str, Any] | None = await ctx.shared_state.get(TOOL_REGISTRY_KEY)
|
||||
tool_registry: dict[str, Any] | None = ctx.state.get(TOOL_REGISTRY_KEY)
|
||||
except KeyError:
|
||||
tool_registry = {}
|
||||
|
||||
@@ -1044,18 +1043,18 @@ class InvokeToolExecutor(DeclarativeActionExecutor):
|
||||
if tool is None:
|
||||
error_msg = f"Tool '{tool_name}' not found in registry"
|
||||
if output_property:
|
||||
await state.set(output_property, {"error": error_msg})
|
||||
state.set(output_property, {"error": error_msg})
|
||||
await ctx.send_message(ActionComplete())
|
||||
return
|
||||
|
||||
# Build parameters
|
||||
params: dict[str, Any] = {}
|
||||
for param_name, param_expression in parameters.items():
|
||||
params[param_name] = await state.eval_if_expression(param_expression)
|
||||
params[param_name] = state.eval_if_expression(param_expression)
|
||||
|
||||
# Add main input if specified
|
||||
if input_expr:
|
||||
params["input"] = await state.eval_if_expression(input_expr)
|
||||
params["input"] = state.eval_if_expression(input_expr)
|
||||
|
||||
try:
|
||||
# Invoke the tool
|
||||
@@ -1068,11 +1067,11 @@ class InvokeToolExecutor(DeclarativeActionExecutor):
|
||||
|
||||
# Store result
|
||||
if output_property:
|
||||
await state.set(output_property, result)
|
||||
state.set(output_property, result)
|
||||
|
||||
except Exception as e:
|
||||
if output_property:
|
||||
await state.set(output_property, {"error": str(e)})
|
||||
state.set(output_property, {"error": str(e)})
|
||||
await ctx.send_message(ActionComplete())
|
||||
return
|
||||
|
||||
|
||||
+36
-36
@@ -52,8 +52,8 @@ class SetValueExecutor(DeclarativeActionExecutor):
|
||||
|
||||
if path:
|
||||
# Evaluate value if it's an expression
|
||||
evaluated_value = await state.eval_if_expression(value)
|
||||
await state.set(path, evaluated_value)
|
||||
evaluated_value = state.eval_if_expression(value)
|
||||
state.set(path, evaluated_value)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -74,8 +74,8 @@ class SetVariableExecutor(DeclarativeActionExecutor):
|
||||
value = self._action_def.get("value")
|
||||
|
||||
if path:
|
||||
evaluated_value = await state.eval_if_expression(value)
|
||||
await state.set(path, evaluated_value)
|
||||
evaluated_value = state.eval_if_expression(value)
|
||||
state.set(path, evaluated_value)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -96,8 +96,8 @@ class SetTextVariableExecutor(DeclarativeActionExecutor):
|
||||
text = self._action_def.get("text", "")
|
||||
|
||||
if path:
|
||||
evaluated_text = await state.eval_if_expression(text)
|
||||
await state.set(path, str(evaluated_text) if evaluated_text is not None else "")
|
||||
evaluated_text = state.eval_if_expression(text)
|
||||
state.set(path, str(evaluated_text) if evaluated_text is not None else "")
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -126,8 +126,8 @@ class SetMultipleVariablesExecutor(DeclarativeActionExecutor):
|
||||
path = assignment.get("path")
|
||||
value = assignment.get("value")
|
||||
if path:
|
||||
evaluated_value = await state.eval_if_expression(value)
|
||||
await state.set(path, evaluated_value)
|
||||
evaluated_value = state.eval_if_expression(value)
|
||||
state.set(path, evaluated_value)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -148,8 +148,8 @@ class AppendValueExecutor(DeclarativeActionExecutor):
|
||||
value = self._action_def.get("value")
|
||||
|
||||
if path:
|
||||
evaluated_value = await state.eval_if_expression(value)
|
||||
await state.append(path, evaluated_value)
|
||||
evaluated_value = state.eval_if_expression(value)
|
||||
state.append(path, evaluated_value)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -170,7 +170,7 @@ class ResetVariableExecutor(DeclarativeActionExecutor):
|
||||
|
||||
if path:
|
||||
# Reset to None/empty
|
||||
await state.set(path, None)
|
||||
state.set(path, None)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -188,9 +188,9 @@ class ClearAllVariablesExecutor(DeclarativeActionExecutor):
|
||||
state = await self._ensure_state_initialized(ctx, trigger)
|
||||
|
||||
# Get state data and clear Local variables
|
||||
state_data = await state.get_state_data()
|
||||
state_data = state.get_state_data()
|
||||
state_data["Local"] = {}
|
||||
await state.set_state_data(state_data)
|
||||
state.set_state_data(state_data)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -217,10 +217,10 @@ class SendActivityExecutor(DeclarativeActionExecutor):
|
||||
|
||||
if isinstance(text, str):
|
||||
# First evaluate any =expression syntax
|
||||
text = await state.eval_if_expression(text)
|
||||
text = state.eval_if_expression(text)
|
||||
# Then interpolate any {Variable.Path} template syntax
|
||||
if isinstance(text, str):
|
||||
text = await state.interpolate_string(text)
|
||||
text = state.interpolate_string(text)
|
||||
|
||||
# Yield the text as workflow output
|
||||
if text:
|
||||
@@ -258,8 +258,8 @@ class EmitEventExecutor(DeclarativeActionExecutor):
|
||||
event_value = event_def.get("data")
|
||||
|
||||
if event_name:
|
||||
evaluated_name = await state.eval_if_expression(event_name)
|
||||
evaluated_value = await state.eval_if_expression(event_value)
|
||||
evaluated_name = state.eval_if_expression(event_name)
|
||||
evaluated_value = state.eval_if_expression(event_value)
|
||||
|
||||
event_data = {
|
||||
"eventName": evaluated_name,
|
||||
@@ -300,16 +300,16 @@ class EditTableExecutor(DeclarativeActionExecutor):
|
||||
|
||||
if table_path:
|
||||
# Get current table value
|
||||
current_table = await state.get(table_path)
|
||||
current_table = state.get(table_path)
|
||||
if current_table is None:
|
||||
current_table = []
|
||||
elif not isinstance(current_table, list):
|
||||
current_table = [current_table]
|
||||
|
||||
if operation == "add" or operation == "insert":
|
||||
evaluated_value = await state.eval_if_expression(value)
|
||||
evaluated_value = state.eval_if_expression(value)
|
||||
if index is not None:
|
||||
evaluated_index = await state.eval_if_expression(index)
|
||||
evaluated_index = state.eval_if_expression(index)
|
||||
idx = int(evaluated_index) if evaluated_index is not None else len(current_table)
|
||||
current_table.insert(idx, evaluated_value)
|
||||
else:
|
||||
@@ -318,12 +318,12 @@ class EditTableExecutor(DeclarativeActionExecutor):
|
||||
elif operation == "remove":
|
||||
if value is not None:
|
||||
# Remove by value
|
||||
evaluated_value = await state.eval_if_expression(value)
|
||||
evaluated_value = state.eval_if_expression(value)
|
||||
if evaluated_value in current_table:
|
||||
current_table.remove(evaluated_value)
|
||||
elif index is not None:
|
||||
# Remove by index
|
||||
evaluated_index = await state.eval_if_expression(index)
|
||||
evaluated_index = state.eval_if_expression(index)
|
||||
idx = int(evaluated_index) if evaluated_index is not None else -1
|
||||
if 0 <= idx < len(current_table):
|
||||
current_table.pop(idx)
|
||||
@@ -334,13 +334,13 @@ class EditTableExecutor(DeclarativeActionExecutor):
|
||||
elif operation == "set" or operation == "update":
|
||||
# Update item at index
|
||||
if index is not None:
|
||||
evaluated_value = await state.eval_if_expression(value)
|
||||
evaluated_index = await state.eval_if_expression(index)
|
||||
evaluated_value = state.eval_if_expression(value)
|
||||
evaluated_index = state.eval_if_expression(index)
|
||||
idx = int(evaluated_index) if evaluated_index is not None else 0
|
||||
if 0 <= idx < len(current_table):
|
||||
current_table[idx] = evaluated_value
|
||||
|
||||
await state.set(table_path, current_table)
|
||||
state.set(table_path, current_table)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -377,16 +377,16 @@ class EditTableV2Executor(DeclarativeActionExecutor):
|
||||
|
||||
if table_path:
|
||||
# Get current table value
|
||||
current_table = await state.get(table_path)
|
||||
current_table = state.get(table_path)
|
||||
if current_table is None:
|
||||
current_table = []
|
||||
elif not isinstance(current_table, list):
|
||||
current_table = [current_table]
|
||||
|
||||
if operation == "add":
|
||||
evaluated_item = await state.eval_if_expression(item)
|
||||
evaluated_item = state.eval_if_expression(item)
|
||||
if index is not None:
|
||||
evaluated_index = await state.eval_if_expression(index)
|
||||
evaluated_index = state.eval_if_expression(index)
|
||||
idx = int(evaluated_index) if evaluated_index is not None else len(current_table)
|
||||
current_table.insert(idx, evaluated_item)
|
||||
else:
|
||||
@@ -394,7 +394,7 @@ class EditTableV2Executor(DeclarativeActionExecutor):
|
||||
|
||||
elif operation == "remove":
|
||||
if item is not None:
|
||||
evaluated_item = await state.eval_if_expression(item)
|
||||
evaluated_item = state.eval_if_expression(item)
|
||||
if key_field and isinstance(evaluated_item, dict):
|
||||
# Remove by key match
|
||||
key_value = evaluated_item.get(key_field)
|
||||
@@ -404,7 +404,7 @@ class EditTableV2Executor(DeclarativeActionExecutor):
|
||||
elif evaluated_item in current_table:
|
||||
current_table.remove(evaluated_item)
|
||||
elif index is not None:
|
||||
evaluated_index = await state.eval_if_expression(index)
|
||||
evaluated_index = state.eval_if_expression(index)
|
||||
idx = int(evaluated_index) if evaluated_index is not None else -1
|
||||
if 0 <= idx < len(current_table):
|
||||
current_table.pop(idx)
|
||||
@@ -413,7 +413,7 @@ class EditTableV2Executor(DeclarativeActionExecutor):
|
||||
current_table = []
|
||||
|
||||
elif operation == "addorupdate":
|
||||
evaluated_item = await state.eval_if_expression(item)
|
||||
evaluated_item = state.eval_if_expression(item)
|
||||
if key_field and isinstance(evaluated_item, dict):
|
||||
key_value = evaluated_item.get(key_field)
|
||||
# Find existing item with same key
|
||||
@@ -433,9 +433,9 @@ class EditTableV2Executor(DeclarativeActionExecutor):
|
||||
current_table.append(evaluated_item)
|
||||
|
||||
elif operation == "update":
|
||||
evaluated_item = await state.eval_if_expression(item)
|
||||
evaluated_item = state.eval_if_expression(item)
|
||||
if index is not None:
|
||||
evaluated_index = await state.eval_if_expression(index)
|
||||
evaluated_index = state.eval_if_expression(index)
|
||||
idx = int(evaluated_index) if evaluated_index is not None else 0
|
||||
if 0 <= idx < len(current_table):
|
||||
current_table[idx] = evaluated_item
|
||||
@@ -446,7 +446,7 @@ class EditTableV2Executor(DeclarativeActionExecutor):
|
||||
current_table[i] = evaluated_item
|
||||
break
|
||||
|
||||
await state.set(table_path, current_table)
|
||||
state.set(table_path, current_table)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -479,13 +479,13 @@ class ParseValueExecutor(DeclarativeActionExecutor):
|
||||
|
||||
if path and value is not None:
|
||||
# Evaluate the value expression
|
||||
evaluated_value = await state.eval_if_expression(value)
|
||||
evaluated_value = state.eval_if_expression(value)
|
||||
|
||||
# Convert to target type if specified
|
||||
if value_type:
|
||||
evaluated_value = self._convert_to_type(evaluated_value, value_type)
|
||||
|
||||
await state.set(path, evaluated_value)
|
||||
state.set(path, evaluated_value)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
|
||||
+20
-20
@@ -7,7 +7,7 @@ Control flow in the graph-based system is handled differently than the interpret
|
||||
returns a ConditionResult with the first-matching branch index. Edge conditions
|
||||
then check the branch_index to route to the correct branch. This ensures only
|
||||
one branch executes (first-match semantics), matching the interpreter behavior.
|
||||
- Foreach: Loop iteration state managed in SharedState + loop edges
|
||||
- Foreach: Loop iteration state managed in State + loop edges
|
||||
- Goto: Edge to target action (handled by builder)
|
||||
- Break/Continue: Special signals for loop control
|
||||
|
||||
@@ -30,7 +30,7 @@ from ._declarative_base import (
|
||||
LoopIterationResult,
|
||||
)
|
||||
|
||||
# Keys for loop state in SharedState
|
||||
# Keys for loop state in State
|
||||
LOOP_STATE_KEY = "_declarative_loop_state"
|
||||
|
||||
# Index value indicating the else/default branch
|
||||
@@ -88,7 +88,7 @@ class ConditionGroupEvaluatorExecutor(DeclarativeActionExecutor):
|
||||
elif isinstance(condition_expr, str) and not condition_expr.startswith("="):
|
||||
condition_expr = f"={condition_expr}"
|
||||
|
||||
result = await state.eval(condition_expr)
|
||||
result = state.eval(condition_expr)
|
||||
if bool(result):
|
||||
# First matching condition found
|
||||
await ctx.send_message(ConditionResult(matched=True, branch_index=index, value=result))
|
||||
@@ -143,7 +143,7 @@ class SwitchEvaluatorExecutor(DeclarativeActionExecutor):
|
||||
return
|
||||
|
||||
# Evaluate the switch value once
|
||||
switch_value = await state.eval_if_expression(value_expr)
|
||||
switch_value = state.eval_if_expression(value_expr)
|
||||
|
||||
# Compare against each case's match value
|
||||
for index, case_item in enumerate(self._cases):
|
||||
@@ -152,7 +152,7 @@ class SwitchEvaluatorExecutor(DeclarativeActionExecutor):
|
||||
continue
|
||||
|
||||
# Evaluate the match value
|
||||
match_value = await state.eval_if_expression(match_expr)
|
||||
match_value = state.eval_if_expression(match_expr)
|
||||
|
||||
if switch_value == match_value:
|
||||
# Found matching case
|
||||
@@ -196,7 +196,7 @@ class IfConditionEvaluatorExecutor(DeclarativeActionExecutor):
|
||||
"""Evaluate the condition and output the result."""
|
||||
state = await self._ensure_state_initialized(ctx, trigger)
|
||||
|
||||
result = await state.eval(self._condition_expr)
|
||||
result = state.eval(self._condition_expr)
|
||||
is_truthy = bool(result)
|
||||
|
||||
if is_truthy:
|
||||
@@ -208,7 +208,7 @@ class IfConditionEvaluatorExecutor(DeclarativeActionExecutor):
|
||||
class ForeachInitExecutor(DeclarativeActionExecutor):
|
||||
"""Initializes a foreach loop.
|
||||
|
||||
Sets up the loop state in SharedState and determines if there are items.
|
||||
Sets up the loop state in State and determines if there are items.
|
||||
"""
|
||||
|
||||
@handler
|
||||
@@ -226,7 +226,7 @@ class ForeachInitExecutor(DeclarativeActionExecutor):
|
||||
items_expr = (
|
||||
self._action_def.get("itemsSource") or self._action_def.get("items") or self._action_def.get("source")
|
||||
)
|
||||
items_raw: Any = await state.eval_if_expression(items_expr) or []
|
||||
items_raw: Any = state.eval_if_expression(items_expr) or []
|
||||
|
||||
items: list[Any]
|
||||
items = (list(items_raw) if items_raw else []) if not isinstance(items_raw, (list, tuple)) else list(items_raw) # type: ignore
|
||||
@@ -234,14 +234,14 @@ class ForeachInitExecutor(DeclarativeActionExecutor):
|
||||
loop_id = self.id
|
||||
|
||||
# Store loop state
|
||||
state_data = await state.get_state_data()
|
||||
state_data = state.get_state_data()
|
||||
loop_states: dict[str, Any] = cast(dict[str, Any], state_data).setdefault(LOOP_STATE_KEY, {})
|
||||
loop_states[loop_id] = {
|
||||
"items": items,
|
||||
"index": 0,
|
||||
"length": len(items),
|
||||
}
|
||||
await state.set_state_data(state_data)
|
||||
state.set_state_data(state_data)
|
||||
|
||||
# Check if we have items
|
||||
if items:
|
||||
@@ -263,9 +263,9 @@ class ForeachInitExecutor(DeclarativeActionExecutor):
|
||||
index_name = self._action_def.get("indexName", "index")
|
||||
index_var = f"Local.{index_name}"
|
||||
|
||||
await state.set(item_var, items[0])
|
||||
state.set(item_var, items[0])
|
||||
if index_var:
|
||||
await state.set(index_var, 0)
|
||||
state.set(index_var, 0)
|
||||
|
||||
await ctx.send_message(LoopIterationResult(has_next=True, current_item=items[0], current_index=0))
|
||||
else:
|
||||
@@ -307,7 +307,7 @@ class ForeachNextExecutor(DeclarativeActionExecutor):
|
||||
loop_id = self._init_executor_id
|
||||
|
||||
# Get loop state
|
||||
state_data = await state.get_state_data()
|
||||
state_data = state.get_state_data()
|
||||
loop_states: dict[str, Any] = cast(dict[str, Any], state_data).get(LOOP_STATE_KEY, {})
|
||||
loop_state = loop_states.get(loop_id)
|
||||
|
||||
@@ -322,7 +322,7 @@ class ForeachNextExecutor(DeclarativeActionExecutor):
|
||||
if current_index < len(items):
|
||||
# Update loop state
|
||||
loop_state["index"] = current_index
|
||||
await state.set_state_data(state_data)
|
||||
state.set_state_data(state_data)
|
||||
|
||||
# Set the iteration variable
|
||||
# Support multiple schema formats:
|
||||
@@ -342,9 +342,9 @@ class ForeachNextExecutor(DeclarativeActionExecutor):
|
||||
index_name = self._action_def.get("indexName", "index")
|
||||
index_var = f"Local.{index_name}"
|
||||
|
||||
await state.set(item_var, items[current_index])
|
||||
state.set(item_var, items[current_index])
|
||||
if index_var:
|
||||
await state.set(index_var, current_index)
|
||||
state.set(index_var, current_index)
|
||||
|
||||
await ctx.send_message(
|
||||
LoopIterationResult(has_next=True, current_item=items[current_index], current_index=current_index)
|
||||
@@ -354,7 +354,7 @@ class ForeachNextExecutor(DeclarativeActionExecutor):
|
||||
loop_states_dict = cast(dict[str, Any], state_data).get(LOOP_STATE_KEY, {})
|
||||
if loop_id in loop_states_dict:
|
||||
del loop_states_dict[loop_id]
|
||||
await state.set_state_data(state_data)
|
||||
state.set_state_data(state_data)
|
||||
|
||||
await ctx.send_message(LoopIterationResult(has_next=False))
|
||||
|
||||
@@ -365,15 +365,15 @@ class ForeachNextExecutor(DeclarativeActionExecutor):
|
||||
ctx: WorkflowContext[LoopIterationResult],
|
||||
) -> None:
|
||||
"""Handle break/continue signals."""
|
||||
state = self._get_state(ctx.shared_state)
|
||||
state = self._get_state(ctx.state)
|
||||
|
||||
if control.action == "break":
|
||||
# Clean up loop state and signal done
|
||||
state_data = await state.get_state_data()
|
||||
state_data = state.get_state_data()
|
||||
loop_states: dict[str, Any] = cast(dict[str, Any], state_data).get(LOOP_STATE_KEY, {})
|
||||
if self._init_executor_id in loop_states:
|
||||
del loop_states[self._init_executor_id]
|
||||
await state.set_state_data(state_data)
|
||||
state.set_state_data(state_data)
|
||||
|
||||
await ctx.send_message(LoopIterationResult(has_next=False))
|
||||
|
||||
|
||||
+14
-14
@@ -84,7 +84,7 @@ class QuestionExecutor(DeclarativeActionExecutor):
|
||||
allow_free_text = self._action_def.get("allowFreeText", True)
|
||||
|
||||
# Evaluate the question text if it's an expression
|
||||
evaluated_question = await state.eval_if_expression(question_text)
|
||||
evaluated_question = state.eval_if_expression(question_text)
|
||||
|
||||
# Build choices metadata
|
||||
choices_data: list[dict[str, str]] | None = None
|
||||
@@ -101,8 +101,8 @@ class QuestionExecutor(DeclarativeActionExecutor):
|
||||
choices_data.append({"value": str(c), "label": str(c)})
|
||||
|
||||
# Store output property in shared state for response handler
|
||||
await ctx.shared_state.set("_question_output_property", output_property)
|
||||
await ctx.shared_state.set("_question_default_value", default_value)
|
||||
ctx.state.set("_question_output_property", output_property)
|
||||
ctx.state.set("_question_default_value", default_value)
|
||||
|
||||
# Request external input - workflow pauses here
|
||||
await ctx.request_info(
|
||||
@@ -128,13 +128,13 @@ class QuestionExecutor(DeclarativeActionExecutor):
|
||||
ctx: WorkflowContext[ActionComplete],
|
||||
) -> None:
|
||||
"""Handle the user's response to the question."""
|
||||
state = self._get_state(ctx.shared_state)
|
||||
state = self._get_state(ctx.state)
|
||||
|
||||
output_property = original_request.metadata.get("output_property", "Local.answer")
|
||||
answer = response.value if response.value is not None else response.user_input
|
||||
|
||||
if output_property:
|
||||
await state.set(output_property, answer)
|
||||
state.set(output_property, answer)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -163,7 +163,7 @@ class ConfirmationExecutor(DeclarativeActionExecutor):
|
||||
default_value = self._action_def.get("defaultValue", False)
|
||||
|
||||
# Evaluate the message if it's an expression
|
||||
evaluated_message = await state.eval_if_expression(message)
|
||||
evaluated_message = state.eval_if_expression(message)
|
||||
|
||||
# Request confirmation - workflow pauses here
|
||||
await ctx.request_info(
|
||||
@@ -189,7 +189,7 @@ class ConfirmationExecutor(DeclarativeActionExecutor):
|
||||
ctx: WorkflowContext[ActionComplete],
|
||||
) -> None:
|
||||
"""Handle the user's confirmation response."""
|
||||
state = self._get_state(ctx.shared_state)
|
||||
state = self._get_state(ctx.state)
|
||||
|
||||
output_property = original_request.metadata.get("output_property", "Local.confirmed")
|
||||
|
||||
@@ -202,7 +202,7 @@ class ConfirmationExecutor(DeclarativeActionExecutor):
|
||||
confirmed = user_input_lower in ("yes", "y", "true", "1", "confirm", "ok")
|
||||
|
||||
if output_property:
|
||||
await state.set(output_property, confirmed)
|
||||
state.set(output_property, confirmed)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -231,7 +231,7 @@ class WaitForInputExecutor(DeclarativeActionExecutor):
|
||||
|
||||
# Emit prompt if specified
|
||||
if prompt:
|
||||
evaluated_prompt = await state.eval_if_expression(prompt)
|
||||
evaluated_prompt = state.eval_if_expression(prompt)
|
||||
await ctx.yield_output(str(evaluated_prompt))
|
||||
|
||||
# Request user input - workflow pauses here
|
||||
@@ -256,12 +256,12 @@ class WaitForInputExecutor(DeclarativeActionExecutor):
|
||||
ctx: WorkflowContext[ActionComplete, str],
|
||||
) -> None:
|
||||
"""Handle the user's input."""
|
||||
state = self._get_state(ctx.shared_state)
|
||||
state = self._get_state(ctx.state)
|
||||
|
||||
output_property = original_request.metadata.get("output_property", "Local.input")
|
||||
|
||||
if output_property:
|
||||
await state.set(output_property, response.user_input)
|
||||
state.set(output_property, response.user_input)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
@@ -292,7 +292,7 @@ class RequestExternalInputExecutor(DeclarativeActionExecutor):
|
||||
metadata = self._action_def.get("metadata", {})
|
||||
|
||||
# Evaluate the message if it's an expression
|
||||
evaluated_message = await state.eval_if_expression(message)
|
||||
evaluated_message = state.eval_if_expression(message)
|
||||
|
||||
# Build request metadata
|
||||
request_metadata: dict[str, Any] = {
|
||||
@@ -323,14 +323,14 @@ class RequestExternalInputExecutor(DeclarativeActionExecutor):
|
||||
ctx: WorkflowContext[ActionComplete],
|
||||
) -> None:
|
||||
"""Handle the external input response."""
|
||||
state = self._get_state(ctx.shared_state)
|
||||
state = self._get_state(ctx.state)
|
||||
|
||||
output_property = original_request.metadata.get("output_property", "Local.externalInput")
|
||||
|
||||
# Store the response value or user_input
|
||||
result = response.value if response.value is not None else response.user_input
|
||||
if output_property:
|
||||
await state.set(output_property, result)
|
||||
state.set(output_property, result)
|
||||
|
||||
await ctx.send_message(ActionComplete())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user