-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Python: Hosted declarative #5530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
910172c
56ab7df
35fa939
dde1edf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -228,6 +228,34 @@ async def test_entry_join_executor_initializes_workflow_inputs_string(self): | |
| outputs = result.get_outputs() | ||
| assert any("hello-world" in str(o) for o in outputs), f"Expected 'hello-world' in outputs but got: {outputs}" | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_as_agent_round_trip_with_last_message_text(self): | ||
| """Regression test: a declarative workflow built via WorkflowFactory must be | ||
| consumable as an AIAgent via Workflow.as_agent(). | ||
|
|
||
| Specifically, the declarative start executor must accept list[Message] | ||
| (the input passed by WorkflowAgent) and populate System.LastMessageText | ||
| so =System.LastMessageText is resolvable in the YAML. | ||
| """ | ||
| factory = WorkflowFactory() | ||
| workflow = factory.create_workflow_from_yaml(""" | ||
| name: as-agent-roundtrip-test | ||
| actions: | ||
| - kind: SetVariable | ||
| variable: Local.echo | ||
| value: =System.LastMessageText | ||
| - kind: SendActivity | ||
| activity: | ||
| text: =Local.echo | ||
| """) | ||
|
|
||
| agent = workflow.as_agent(name="echo-agent") | ||
| response = await agent.run("Hello there") | ||
|
|
||
| assert "Hello there" in response.text, ( | ||
| f"Expected 'Hello there' in agent response text but got: {response.text!r}" | ||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test only exercises the simplest case: a single user string through |
||
|
|
||
|
|
||
| class TestWorkflowFactoryAgentRegistration: | ||
| """Tests for agent registration.""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -256,6 +256,19 @@ async def _handle_inner_workflow( | |
| input_messages = _items_to_messages(input_items) | ||
| is_streaming_request = request.stream is not None and request.stream is True | ||
|
|
||
| # Fetch prior conversation history from Foundry storage so workflow | ||
| # agents see the same history their non-workflow counterparts get | ||
| # (see _handle_inner_agent which builds messages from history + | ||
| # current input). Without this, declarative workflows triggered via | ||
| # WorkflowAgent.as_agent only ever see the latest user turn, even | ||
| # though the host's checkpoint replay restores the workflow's | ||
| # internal state - declarative workflows reset Conversation.messages | ||
| # on every new run, so cross-turn context has to come from the | ||
| # message list passed in, not from checkpointed workflow state. | ||
| history = await context.get_history() | ||
| history_messages = _output_items_to_messages(history) | ||
| full_messages = [*history_messages, *input_messages] | ||
|
|
||
| _, are_options_set = _to_chat_options(request) | ||
| if are_options_set: | ||
| logger.warning("Workflow agent doesn't support runtime options. They will be ignored.") | ||
|
|
@@ -307,7 +320,7 @@ async def _handle_inner_workflow( | |
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing |
||
| if not is_streaming_request: | ||
| # Run the agent in non-streaming mode | ||
| response = await self._agent.run(input_messages, stream=False, checkpoint_storage=checkpoint_storage) | ||
| response = await self._agent.run(full_messages, stream=False, checkpoint_storage=checkpoint_storage) | ||
|
|
||
| for message in response.messages: | ||
| for content in message.contents: | ||
|
|
@@ -323,7 +336,7 @@ async def _handle_inner_workflow( | |
| tracker = _OutputItemTracker(response_event_stream) | ||
|
|
||
| # Run the workflow agent in streaming mode | ||
| async for update in self._agent.run(input_messages, stream=True, checkpoint_storage=checkpoint_storage): | ||
| async for update in self._agent.run(full_messages, stream=True, checkpoint_storage=checkpoint_storage): | ||
| for content in update.contents: | ||
| for event in tracker.handle(content): | ||
| yield event | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reduces the newest user turn to
last_user_msg.text, butMessage.textonly concatenates text content. Downstream,InvokeAzureAgentExecutorreconstructs the turn asMessage(role="user", contents=[input_text]), so images, tool responses, approvals, or any other non-text content on the current turn are silently dropped. A safer design is to keep the fullMessagein state and only deriveSystem.LastMessageTextas a compatibility view.