Skip to content

Commit b355a43

Browse files
alliscodealliscodeCopilot
authored
Hosted declarative (#5530)
* Fix declarative Workflow.as_agent() by accepting list[Message] in start executor The declarative start executor (JoinExecutor) only advertised dict and str in its input_types, so WorkflowAgent.__init__ rejected it with 'Workflow's start executor cannot handle list[Message]'. Add list[Message] to the JoinExecutor handler annotation and add a matching branch in DeclarativeActionExecutor._ensure_state_initialized that extracts the last user-message text and falls through to the string-input initialization path, so =System.LastMessageText works end-to-end via as_agent(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Populate Conversation.messages from list[Message] trigger When Workflow.as_agent() is invoked with a list[Message], the start executor now populates Conversation.messages / Conversation.history / System.conversations.{id}.messages with prior turns only (excluding the latest user message), and surfaces the latest user message via Inputs.input and System.LastMessage*. This matches InvokeAzureAgent's contract that the messages binding holds prior turns and the executor itself appends the new user input before invoking, avoiding double-append of the trailing user turn while preserving full history (incl. assistant/system/tool roles and multi-modal content) for downstream actions. * Coerce Enum values when serializing PowerFx symbols MessageRole and other str-subclass Enums passed isinstance(v, str) and were forwarded to pythonnet unchanged. pythonnet then raised 'MessageRole value cannot be converted to System.String' for every PowerFx primitive when ConditionGroup/Expr eval walked the symbol table containing Conversation.messages. Reduce Enum members to their underlying value before the primitive check so eval sees plain strings/ints. * Foundry hosting: pass full conversation history to workflow agents _handle_inner_workflow only forwarded the latest user turn to WorkflowAgent.run, even though _handle_inner_agent already prepends history fetched from Foundry storage to the messages it sends a regular agent. Declarative workflows reset Conversation.messages on every run (state.initialize), so checkpoint replay alone does not give them prior turns - the host has to pass them in, the same way it does for non-workflow agents. Mirror that contract: fetch context.get_history() and pass [*history, *input_messages] to the workflow agent. --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8837df1 commit b355a43

2 files changed

Lines changed: 30 additions & 3 deletions

File tree

python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import logging
3030
import sys
3131
import uuid
32+
from enum import Enum
3233
from collections.abc import Mapping
3334
from dataclasses import dataclass
3435
from decimal import Decimal as _Decimal
@@ -121,7 +122,20 @@ def _make_powerfx_safe(value: Any) -> Any:
121122
Returns:
122123
A PowerFx-safe representation of the value
123124
"""
124-
if value is None or isinstance(value, _POWERFX_SAFE_TYPES):
125+
if value is None:
126+
return value
127+
128+
# Enum coercion must run BEFORE the primitive type check: many MAF
129+
# enums (e.g. MessageRole) are ``str``-subclass enums, so they pass
130+
# ``isinstance(v, str)`` but pythonnet refuses to convert them to
131+
# ``System.String`` and raises ``'MessageRole' value cannot be
132+
# converted to System.<X>'`` for every PowerFx primitive type. Reduce
133+
# to the underlying value (or its string form) so PowerFx sees a
134+
# plain ``str``/``int``.
135+
if isinstance(value, Enum):
136+
return _make_powerfx_safe(value.value)
137+
138+
if isinstance(value, _POWERFX_SAFE_TYPES):
125139
return value
126140

127141
if isinstance(value, dict):

python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,19 @@ async def _handle_inner_workflow(
256256
input_messages = _items_to_messages(input_items)
257257
is_streaming_request = request.stream is not None and request.stream is True
258258

259+
# Fetch prior conversation history from Foundry storage so workflow
260+
# agents see the same history their non-workflow counterparts get
261+
# (see _handle_inner_agent which builds messages from history +
262+
# current input). Without this, declarative workflows triggered via
263+
# WorkflowAgent.as_agent only ever see the latest user turn, even
264+
# though the host's checkpoint replay restores the workflow's
265+
# internal state - declarative workflows reset Conversation.messages
266+
# on every new run, so cross-turn context has to come from the
267+
# message list passed in, not from checkpointed workflow state.
268+
history = await context.get_history()
269+
history_messages = _output_items_to_messages(history)
270+
full_messages = [*history_messages, *input_messages]
271+
259272
_, are_options_set = _to_chat_options(request)
260273
if are_options_set:
261274
logger.warning("Workflow agent doesn't support runtime options. They will be ignored.")
@@ -307,7 +320,7 @@ async def _handle_inner_workflow(
307320

308321
if not is_streaming_request:
309322
# Run the agent in non-streaming mode
310-
response = await self._agent.run(input_messages, stream=False, checkpoint_storage=checkpoint_storage)
323+
response = await self._agent.run(full_messages, stream=False, checkpoint_storage=checkpoint_storage)
311324

312325
for message in response.messages:
313326
for content in message.contents:
@@ -323,7 +336,7 @@ async def _handle_inner_workflow(
323336
tracker = _OutputItemTracker(response_event_stream)
324337

325338
# Run the workflow agent in streaming mode
326-
async for update in self._agent.run(input_messages, stream=True, checkpoint_storage=checkpoint_storage):
339+
async for update in self._agent.run(full_messages, stream=True, checkpoint_storage=checkpoint_storage):
327340
for content in update.contents:
328341
for event in tracker.handle(content):
329342
yield event

0 commit comments

Comments
 (0)