mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Merge send_responses into run method (#3720)
* Streamline workflow run api with send responses in one method * Fixes * Address copilot feedback
This commit is contained in:
committed by
GitHub
Unverified
parent
15256bb616
commit
a17f13598b
@@ -340,25 +340,22 @@ class WorkflowAgent(BaseAgent):
|
||||
Yields:
|
||||
WorkflowEvent objects from the workflow execution.
|
||||
"""
|
||||
# Determine the execution mode based on state
|
||||
# Determine the execution mode based on state.
|
||||
# The streaming flag controls the workflow's internal streaming mode,
|
||||
# which affects executor behavior (e.g. AgentExecutor emits different event
|
||||
# types in streaming vs non-streaming mode).
|
||||
if bool(self.pending_requests):
|
||||
# This is a continuation - send function responses back
|
||||
function_responses = self._process_pending_requests(input_messages)
|
||||
|
||||
if streaming:
|
||||
async for event in self.workflow.send_responses_streaming(function_responses):
|
||||
async for event in self.workflow.run(responses=function_responses, stream=True, **kwargs):
|
||||
yield event
|
||||
else:
|
||||
workflow_result = await self.workflow.send_responses(function_responses)
|
||||
for event in workflow_result:
|
||||
for event in await self.workflow.run(responses=function_responses, **kwargs):
|
||||
yield event
|
||||
|
||||
elif checkpoint_id is not None:
|
||||
# Resume from checkpoint - don't prepend thread history since workflow state
|
||||
# is being restored from the checkpoint
|
||||
if streaming:
|
||||
async for event in self.workflow.run(
|
||||
message=None,
|
||||
stream=True,
|
||||
checkpoint_id=checkpoint_id,
|
||||
checkpoint_storage=checkpoint_storage,
|
||||
@@ -366,19 +363,15 @@ class WorkflowAgent(BaseAgent):
|
||||
):
|
||||
yield event
|
||||
else:
|
||||
workflow_result = await self.workflow.run(
|
||||
message=None,
|
||||
for event in await self.workflow.run(
|
||||
checkpoint_id=checkpoint_id,
|
||||
checkpoint_storage=checkpoint_storage,
|
||||
**kwargs,
|
||||
)
|
||||
for event in workflow_result:
|
||||
):
|
||||
yield event
|
||||
|
||||
else:
|
||||
# Initial run - build conversation from thread history
|
||||
conversation_messages = await self._build_conversation_messages(thread, input_messages)
|
||||
|
||||
if streaming:
|
||||
async for event in self.workflow.run(
|
||||
message=conversation_messages,
|
||||
@@ -388,12 +381,11 @@ class WorkflowAgent(BaseAgent):
|
||||
):
|
||||
yield event
|
||||
else:
|
||||
workflow_result = await self.workflow.run(
|
||||
for event in await self.workflow.run(
|
||||
message=conversation_messages,
|
||||
checkpoint_storage=checkpoint_storage,
|
||||
**kwargs,
|
||||
)
|
||||
for event in workflow_result:
|
||||
):
|
||||
yield event
|
||||
|
||||
# endregion Run Methods
|
||||
|
||||
@@ -177,6 +177,54 @@ def is_instance_of(data: Any, target_type: type | UnionType | Any) -> bool:
|
||||
return isinstance(data, target_type)
|
||||
|
||||
|
||||
def try_coerce_to_type(data: Any, target_type: type | UnionType | Any) -> Any:
|
||||
"""Try to coerce data to the target type.
|
||||
|
||||
Attempts lightweight type coercion for common cases where raw data
|
||||
(e.g., from JSON deserialization) needs to be converted to the expected type.
|
||||
|
||||
Returns the coerced value if successful, or the original value if coercion
|
||||
is not needed or not possible.
|
||||
|
||||
Args:
|
||||
data: The data to coerce.
|
||||
target_type: The type to coerce to.
|
||||
|
||||
Returns:
|
||||
The coerced value, or the original value if coercion fails.
|
||||
"""
|
||||
# If already the right type, return as-is
|
||||
if is_instance_of(data, target_type):
|
||||
return data
|
||||
|
||||
# Can't coerce to non-concrete targets (Union, generic, etc.)
|
||||
if not isinstance(target_type, type):
|
||||
return data
|
||||
|
||||
# int -> float (JSON integers for float fields)
|
||||
if isinstance(data, int) and target_type is float:
|
||||
return float(data)
|
||||
|
||||
# dict -> dataclass
|
||||
if isinstance(data, dict):
|
||||
from dataclasses import is_dataclass
|
||||
|
||||
if is_dataclass(target_type):
|
||||
try:
|
||||
return target_type(**data)
|
||||
except (TypeError, ValueError):
|
||||
return data
|
||||
|
||||
# dict -> Pydantic model
|
||||
if hasattr(target_type, "model_validate"):
|
||||
try:
|
||||
return target_type.model_validate(data)
|
||||
except Exception:
|
||||
return data
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def serialize_type(t: type) -> str:
|
||||
"""Serialize a type to a string.
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ import json
|
||||
import logging
|
||||
import types
|
||||
import uuid
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable, Sequence
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
from .._types import ResponseStream
|
||||
from ..observability import OtelAttr, capture_exception, create_workflow_span
|
||||
from ._agent import WorkflowAgent
|
||||
from ._checkpoint import CheckpointStorage
|
||||
@@ -31,7 +32,7 @@ from ._model_utils import DictConvertible
|
||||
from ._runner import Runner
|
||||
from ._runner_context import RunnerContext
|
||||
from ._state import State
|
||||
from ._typing_utils import is_instance_of
|
||||
from ._typing_utils import is_instance_of, try_coerce_to_type
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -144,7 +145,7 @@ class Workflow(DictConvertible):
|
||||
2. Executor implements `response_handler()` to process the response
|
||||
3. Requests are emitted as request_info events (WorkflowEvent with type='request_info') in the event stream
|
||||
4. Workflow enters IDLE_WITH_PENDING_REQUESTS state
|
||||
5. Caller handles requests and provides responses via the `send_responses` or `send_responses_streaming` methods
|
||||
5. Caller handles requests and provides responses via `run(responses=...)` or `run(responses=..., stream=True)`
|
||||
6. Responses are routed to the requesting executors and response handlers are invoked
|
||||
|
||||
## Checkpointing
|
||||
@@ -450,186 +451,143 @@ class Workflow(DictConvertible):
|
||||
message: Any | None = None,
|
||||
*,
|
||||
stream: Literal[True],
|
||||
responses: dict[str, Any] | None = None,
|
||||
checkpoint_id: str | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[WorkflowEvent]: ...
|
||||
) -> ResponseStream[WorkflowEvent, WorkflowRunResult]: ...
|
||||
|
||||
@overload
|
||||
async def run(
|
||||
def run(
|
||||
self,
|
||||
message: Any | None = None,
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
responses: dict[str, Any] | None = None,
|
||||
checkpoint_id: str | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
include_status_events: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> WorkflowRunResult: ...
|
||||
) -> Awaitable[WorkflowRunResult]: ...
|
||||
|
||||
def run(
|
||||
self,
|
||||
message: Any | None = None,
|
||||
*,
|
||||
stream: bool = False,
|
||||
responses: dict[str, Any] | None = None,
|
||||
checkpoint_id: str | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
include_status_events: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[WorkflowEvent] | Awaitable[WorkflowRunResult]:
|
||||
) -> ResponseStream[WorkflowEvent, WorkflowRunResult] | Awaitable[WorkflowRunResult]:
|
||||
"""Run the workflow, optionally streaming events.
|
||||
|
||||
Unified interface supporting initial runs and checkpoint restoration.
|
||||
Unified interface supporting initial runs, checkpoint restoration, and
|
||||
sending responses to pending requests.
|
||||
|
||||
Args:
|
||||
message: Initial message for the start executor. Required for new workflow runs,
|
||||
should be None when resuming from checkpoint.
|
||||
stream: If True, returns an async iterable of events. If False (default),
|
||||
returns an awaitable WorkflowRunResult.
|
||||
checkpoint_id: ID of checkpoint to restore from. If provided, the workflow resumes
|
||||
from this checkpoint instead of starting fresh.
|
||||
message: Initial message for the start executor. Required for new workflow runs.
|
||||
Mutually exclusive with responses.
|
||||
stream: If True, returns a ResponseStream of events with
|
||||
``get_final_response()`` for the final WorkflowRunResult. If False
|
||||
(default), returns an awaitable WorkflowRunResult.
|
||||
responses: Responses to send for pending request info events, where keys are
|
||||
request IDs and values are the corresponding response data. Mutually
|
||||
exclusive with message. Can be combined with checkpoint_id to restore
|
||||
a checkpoint and send responses in a single call.
|
||||
checkpoint_id: ID of checkpoint to restore from. Can be used alone (resume
|
||||
from checkpoint), with message (not allowed), or with responses
|
||||
(restore then send responses).
|
||||
checkpoint_storage: Runtime checkpoint storage.
|
||||
include_status_events: Whether to include WorkflowStatusEvent instances (non-streaming only).
|
||||
include_status_events: Whether to include status events (non-streaming only).
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
|
||||
Returns:
|
||||
When stream=True: An AsyncIterable[WorkflowEvent] for streaming events.
|
||||
When stream=True: A ResponseStream[WorkflowEvent, WorkflowRunResult] for
|
||||
streaming events. Iterate for events, call get_final_response() for result.
|
||||
When stream=False: An Awaitable[WorkflowRunResult] with all events.
|
||||
|
||||
Raises:
|
||||
ValueError: If both message and checkpoint_id are provided, or if neither is provided.
|
||||
ValueError: If parameter combination is invalid.
|
||||
"""
|
||||
if stream:
|
||||
return self._run_streaming(
|
||||
# Validate parameters and set running flag eagerly (before any async work)
|
||||
self._validate_run_params(message, responses, checkpoint_id)
|
||||
self._ensure_not_running()
|
||||
|
||||
response_stream = ResponseStream[WorkflowEvent, WorkflowRunResult](
|
||||
self._run_core(
|
||||
message=message,
|
||||
responses=responses,
|
||||
checkpoint_id=checkpoint_id,
|
||||
checkpoint_storage=checkpoint_storage,
|
||||
streaming=stream,
|
||||
**kwargs,
|
||||
)
|
||||
return self._run_non_streaming(
|
||||
message=message,
|
||||
checkpoint_id=checkpoint_id,
|
||||
checkpoint_storage=checkpoint_storage,
|
||||
include_status_events=include_status_events,
|
||||
**kwargs,
|
||||
),
|
||||
finalizer=functools.partial(self._finalize_events, include_status_events=include_status_events),
|
||||
cleanup_hooks=[
|
||||
functools.partial(self._run_cleanup, checkpoint_storage),
|
||||
],
|
||||
)
|
||||
|
||||
async def _run_streaming(
|
||||
if stream:
|
||||
return response_stream
|
||||
return response_stream.get_final_response()
|
||||
|
||||
async def _run_core(
|
||||
self,
|
||||
message: Any | None = None,
|
||||
*,
|
||||
responses: dict[str, Any] | None = None,
|
||||
checkpoint_id: str | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
streaming: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[WorkflowEvent]:
|
||||
"""Internal streaming implementation."""
|
||||
# Validate mutually exclusive parameters BEFORE setting running flag
|
||||
if message is not None and checkpoint_id is not None:
|
||||
raise ValueError("Cannot provide both 'message' and 'checkpoint_id'. Use one or the other.")
|
||||
|
||||
if message is None and checkpoint_id is None:
|
||||
raise ValueError("Must provide either 'message' (new run) or 'checkpoint_id' (resume).")
|
||||
|
||||
self._ensure_not_running()
|
||||
|
||||
# Enable runtime checkpointing if storage provided
|
||||
# Two cases:
|
||||
# 1. checkpoint_storage + checkpoint_id: Load checkpoint from this storage and resume
|
||||
# 2. checkpoint_storage without checkpoint_id: Enable checkpointing for this run
|
||||
if checkpoint_storage is not None:
|
||||
self._runner.context.set_runtime_checkpoint_storage(checkpoint_storage)
|
||||
|
||||
try:
|
||||
# Reset context only for new runs (not checkpoint restoration)
|
||||
reset_context = message is not None and checkpoint_id is None
|
||||
|
||||
async for event in self._run_workflow_with_tracing(
|
||||
initial_executor_fn=functools.partial(
|
||||
self._execute_with_message_or_checkpoint, message, checkpoint_id, checkpoint_storage
|
||||
),
|
||||
reset_context=reset_context,
|
||||
streaming=True,
|
||||
run_kwargs=kwargs if kwargs else None,
|
||||
):
|
||||
if event.type == "output" and not self._should_yield_output_event(event):
|
||||
continue
|
||||
yield event
|
||||
finally:
|
||||
if checkpoint_storage is not None:
|
||||
self._runner.context.clear_runtime_checkpoint_storage()
|
||||
self._reset_running_flag()
|
||||
|
||||
async def send_responses_streaming(self, responses: dict[str, Any]) -> AsyncIterable[WorkflowEvent]:
|
||||
"""Send responses back to the workflow and stream the events generated by the workflow.
|
||||
|
||||
Args:
|
||||
responses: The responses to be sent back to the workflow, where keys are request IDs
|
||||
and values are the corresponding response data.
|
||||
"""Single core execution path for both streaming and non-streaming modes.
|
||||
|
||||
Yields:
|
||||
WorkflowEvent: The events generated during the workflow execution after sending the responses.
|
||||
WorkflowEvent: The events generated during the workflow execution.
|
||||
"""
|
||||
self._ensure_not_running()
|
||||
try:
|
||||
async for event in self._run_workflow_with_tracing(
|
||||
initial_executor_fn=functools.partial(self._send_responses_internal, responses),
|
||||
reset_context=False, # Don't reset context when sending responses
|
||||
streaming=True,
|
||||
):
|
||||
if event.type == "output" and not self._should_yield_output_event(event):
|
||||
continue
|
||||
yield event
|
||||
finally:
|
||||
self._reset_running_flag()
|
||||
|
||||
async def _run_non_streaming(
|
||||
self,
|
||||
message: Any | None = None,
|
||||
*,
|
||||
checkpoint_id: str | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
include_status_events: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> WorkflowRunResult:
|
||||
"""Internal non-streaming implementation."""
|
||||
# Validate mutually exclusive parameters BEFORE setting running flag
|
||||
if message is not None and checkpoint_id is not None:
|
||||
raise ValueError("Cannot provide both 'message' and 'checkpoint_id'. Use one or the other.")
|
||||
|
||||
if message is None and checkpoint_id is None:
|
||||
raise ValueError("Must provide either 'message' (new run) or 'checkpoint_id' (resume).")
|
||||
|
||||
self._ensure_not_running()
|
||||
|
||||
# Enable runtime checkpointing if storage provided
|
||||
if checkpoint_storage is not None:
|
||||
self._runner.context.set_runtime_checkpoint_storage(checkpoint_storage)
|
||||
|
||||
try:
|
||||
# Reset context only for new runs (not checkpoint restoration)
|
||||
reset_context = message is not None and checkpoint_id is None
|
||||
initial_executor_fn, reset_context = self._resolve_execution_mode(
|
||||
message, responses, checkpoint_id, checkpoint_storage
|
||||
)
|
||||
|
||||
raw_events = [
|
||||
event
|
||||
async for event in self._run_workflow_with_tracing(
|
||||
initial_executor_fn=functools.partial(
|
||||
self._execute_with_message_or_checkpoint, message, checkpoint_id, checkpoint_storage
|
||||
),
|
||||
reset_context=reset_context,
|
||||
run_kwargs=kwargs if kwargs else None,
|
||||
)
|
||||
]
|
||||
finally:
|
||||
if checkpoint_storage is not None:
|
||||
self._runner.context.clear_runtime_checkpoint_storage()
|
||||
self._reset_running_flag()
|
||||
async for event in self._run_workflow_with_tracing(
|
||||
initial_executor_fn=initial_executor_fn,
|
||||
reset_context=reset_context,
|
||||
streaming=streaming,
|
||||
run_kwargs=kwargs if kwargs else None,
|
||||
):
|
||||
if event.type == "output" and not self._should_yield_output_event(event):
|
||||
continue
|
||||
yield event
|
||||
|
||||
# Filter events for non-streaming mode
|
||||
filtered: list[WorkflowEvent[Any]] = []
|
||||
status_events: list[WorkflowEvent[Any]] = []
|
||||
async def _run_cleanup(self, checkpoint_storage: CheckpointStorage | None) -> None:
|
||||
"""Cleanup hook called after stream consumption."""
|
||||
if checkpoint_storage is not None:
|
||||
self._runner.context.clear_runtime_checkpoint_storage()
|
||||
self._reset_running_flag()
|
||||
|
||||
for ev in raw_events:
|
||||
# Omit started events from non-streaming (telemetry-only)
|
||||
@staticmethod
|
||||
def _finalize_events(
|
||||
events: Sequence[WorkflowEvent],
|
||||
*,
|
||||
include_status_events: bool = False,
|
||||
) -> WorkflowRunResult:
|
||||
"""Convert collected workflow events into a WorkflowRunResult.
|
||||
|
||||
Filters out internal events for non-streaming callers.
|
||||
"""
|
||||
filtered: list[WorkflowEvent] = []
|
||||
status_events: list[WorkflowEvent] = []
|
||||
|
||||
for ev in events:
|
||||
# Omit started events from result (telemetry-only)
|
||||
if ev.type == "started":
|
||||
continue
|
||||
# Track status; include inline only if explicitly requested
|
||||
@@ -638,41 +596,88 @@ class Workflow(DictConvertible):
|
||||
if include_status_events:
|
||||
filtered.append(ev)
|
||||
continue
|
||||
if ev.type == "output" and not self._should_yield_output_event(ev):
|
||||
continue
|
||||
filtered.append(ev)
|
||||
|
||||
return WorkflowRunResult(filtered, status_events)
|
||||
|
||||
async def send_responses(self, responses: dict[str, Any]) -> WorkflowRunResult:
|
||||
"""Send responses back to the workflow.
|
||||
@staticmethod
|
||||
def _validate_run_params(
|
||||
message: Any | None,
|
||||
responses: dict[str, Any] | None,
|
||||
checkpoint_id: str | None,
|
||||
) -> None:
|
||||
"""Validate parameter combinations for run().
|
||||
|
||||
Args:
|
||||
responses: A dictionary where keys are request IDs and values are the corresponding response data.
|
||||
Rules:
|
||||
- message and responses are mutually exclusive
|
||||
- message and checkpoint_id are mutually exclusive
|
||||
- At least one of message, responses, or checkpoint_id must be provided
|
||||
- responses + checkpoint_id is allowed (restore then send)
|
||||
"""
|
||||
if message is not None and responses is not None:
|
||||
raise ValueError("Cannot provide both 'message' and 'responses'. Use one or the other.")
|
||||
|
||||
if message is not None and checkpoint_id is not None:
|
||||
raise ValueError("Cannot provide both 'message' and 'checkpoint_id'. Use one or the other.")
|
||||
|
||||
if message is None and responses is None and checkpoint_id is None:
|
||||
raise ValueError(
|
||||
"Must provide at least one of: 'message' (new run), 'responses' (send responses), "
|
||||
"or 'checkpoint_id' (resume from checkpoint)."
|
||||
)
|
||||
|
||||
def _resolve_execution_mode(
|
||||
self,
|
||||
message: Any | None,
|
||||
responses: dict[str, Any] | None,
|
||||
checkpoint_id: str | None,
|
||||
checkpoint_storage: CheckpointStorage | None,
|
||||
) -> tuple[Callable[[], Awaitable[None]], bool]:
|
||||
"""Determine the initial executor function and reset_context flag based on parameters.
|
||||
|
||||
Returns:
|
||||
A WorkflowRunResult instance containing a list of events generated during the workflow execution.
|
||||
A tuple of (initial_executor_fn, reset_context).
|
||||
"""
|
||||
self._ensure_not_running()
|
||||
try:
|
||||
events = [
|
||||
event
|
||||
async for event in self._run_workflow_with_tracing(
|
||||
initial_executor_fn=functools.partial(self._send_responses_internal, responses),
|
||||
reset_context=False, # Don't reset context when sending responses
|
||||
if responses is not None:
|
||||
if checkpoint_id is not None:
|
||||
# Combined: restore checkpoint then send responses
|
||||
initial_executor_fn = functools.partial(
|
||||
self._restore_and_send_responses, checkpoint_id, checkpoint_storage, responses
|
||||
)
|
||||
]
|
||||
status_events = [e for e in events if e.type == "status"]
|
||||
filtered_events: list[WorkflowEvent[Any]] = []
|
||||
for e in events:
|
||||
if e.type == "output" and not self._should_yield_output_event(e):
|
||||
continue
|
||||
if e.type in ("status", "started"):
|
||||
continue
|
||||
filtered_events.append(e)
|
||||
return WorkflowRunResult(filtered_events, status_events)
|
||||
finally:
|
||||
self._reset_running_flag()
|
||||
else:
|
||||
# Send responses only (requires pending requests in workflow state)
|
||||
initial_executor_fn = functools.partial(self._send_responses_internal, responses)
|
||||
return initial_executor_fn, False
|
||||
# Regular run or checkpoint restoration
|
||||
initial_executor_fn = functools.partial(
|
||||
self._execute_with_message_or_checkpoint, message, checkpoint_id, checkpoint_storage
|
||||
)
|
||||
reset_context = message is not None and checkpoint_id is None
|
||||
return initial_executor_fn, reset_context
|
||||
|
||||
async def _restore_and_send_responses(
|
||||
self,
|
||||
checkpoint_id: str,
|
||||
checkpoint_storage: CheckpointStorage | None,
|
||||
responses: dict[str, Any],
|
||||
) -> None:
|
||||
"""Restore from a checkpoint then send responses to pending requests.
|
||||
|
||||
Args:
|
||||
checkpoint_id: ID of checkpoint to restore from.
|
||||
checkpoint_storage: Runtime checkpoint storage.
|
||||
responses: Responses to send after restoration.
|
||||
"""
|
||||
has_checkpointing = self._runner.context.has_checkpointing()
|
||||
|
||||
if not has_checkpointing and checkpoint_storage is None:
|
||||
raise ValueError(
|
||||
"Cannot restore from checkpoint: either provide checkpoint_storage parameter "
|
||||
"or build workflow with WorkflowBuilder.with_checkpointing(checkpoint_storage)."
|
||||
)
|
||||
|
||||
await self._runner.restore_from_checkpoint(checkpoint_id, checkpoint_storage)
|
||||
await self._send_responses_internal(responses)
|
||||
|
||||
async def _send_responses_internal(self, responses: dict[str, Any]) -> None:
|
||||
"""Internal method to validate and send responses to the executors."""
|
||||
@@ -680,20 +685,24 @@ class Workflow(DictConvertible):
|
||||
if not pending_requests:
|
||||
raise RuntimeError("No pending requests found in workflow context.")
|
||||
|
||||
# Validate responses against pending requests
|
||||
# Validate and coerce responses against pending requests
|
||||
coerced_responses: dict[str, Any] = {}
|
||||
for request_id, response in responses.items():
|
||||
if request_id not in pending_requests:
|
||||
raise ValueError(f"Response provided for unknown request ID: {request_id}")
|
||||
pending_request = pending_requests[request_id]
|
||||
# Try to coerce raw values (e.g., dicts from JSON) to the expected type
|
||||
response = try_coerce_to_type(response, pending_request.response_type)
|
||||
if not is_instance_of(response, pending_request.response_type):
|
||||
raise ValueError(
|
||||
f"Response type mismatch for request ID {request_id}: "
|
||||
f"expected {pending_request.response_type}, got {type(response)}"
|
||||
)
|
||||
coerced_responses[request_id] = response
|
||||
|
||||
await asyncio.gather(*[
|
||||
self._runner_context.send_request_info_response(request_id, response)
|
||||
for request_id, response in responses.items()
|
||||
for request_id, response in coerced_responses.items()
|
||||
])
|
||||
|
||||
def _get_executor_by_id(self, executor_id: str) -> Executor:
|
||||
|
||||
@@ -653,7 +653,7 @@ class WorkflowExecutor(Executor):
|
||||
|
||||
try:
|
||||
# Resume the sub-workflow with all collected responses
|
||||
result = await self.workflow.send_responses(responses_to_send)
|
||||
result = await self.workflow.run(responses=responses_to_send)
|
||||
# Remove handled requests from result. The result may contain the original
|
||||
# RequestInfoEvents that were already handled. This is due to checkpointing
|
||||
# and rehydration of the workflow that re-adds the RequestInfoEvents to the
|
||||
|
||||
@@ -267,9 +267,9 @@ async def test_agent_executor_tool_call_with_approval() -> None:
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
events = await workflow.send_responses({
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True)
|
||||
})
|
||||
events = await workflow.run(
|
||||
responses={approval_request.request_id: approval_request.data.to_function_approval_response(True)}
|
||||
)
|
||||
|
||||
# Assert
|
||||
final_response = events.get_outputs()
|
||||
@@ -303,9 +303,9 @@ async def test_agent_executor_tool_call_with_approval_streaming() -> None:
|
||||
|
||||
# Act
|
||||
output: str | None = None
|
||||
async for event in workflow.send_responses_streaming({
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True)
|
||||
}):
|
||||
async for event in workflow.run(
|
||||
stream=True, responses={approval_request.request_id: approval_request.data.to_function_approval_response(True)}
|
||||
):
|
||||
if event.type == "output":
|
||||
output = event.data
|
||||
|
||||
@@ -346,7 +346,7 @@ async def test_agent_executor_parallel_tool_call_with_approval() -> None:
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True) # type: ignore
|
||||
for approval_request in events.get_request_info_events()
|
||||
}
|
||||
events = await workflow.send_responses(responses)
|
||||
events = await workflow.run(responses=responses)
|
||||
|
||||
# Assert
|
||||
final_response = events.get_outputs()
|
||||
@@ -385,7 +385,7 @@ async def test_agent_executor_parallel_tool_call_with_approval_streaming() -> No
|
||||
}
|
||||
|
||||
output: str | None = None
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
async for event in workflow.run(stream=True, responses=responses):
|
||||
if event.type == "output":
|
||||
output = event.data
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Send response and continue workflow
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming({request_info_event.request_id: True}):
|
||||
async for event in workflow.run(stream=True, responses={request_info_event.request_id: True}):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -219,7 +219,7 @@ class TestRequestInfoAndResponse:
|
||||
# Send response with calculated result
|
||||
calculated_result = 31.0
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming({request_info_event.request_id: calculated_result}):
|
||||
async for event in workflow.run(stream=True, responses={request_info_event.request_id: calculated_result}):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -254,7 +254,7 @@ class TestRequestInfoAndResponse:
|
||||
# Send responses for both requests
|
||||
responses = {approval_event.request_id: True, calc_event.request_id: 50.0}
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
async for event in workflow.run(stream=True, responses=responses):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -276,7 +276,7 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Deny the request
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming({request_info_event.request_id: False}):
|
||||
async for event in workflow.run(stream=True, responses={request_info_event.request_id: False}):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -303,7 +303,7 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Continue with response
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming({request_info_event.request_id: True}):
|
||||
async for event in workflow.run(stream=True, responses={request_info_event.request_id: True}):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -395,9 +395,12 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Step 6: Provide response to the restored request and complete the workflow
|
||||
final_completed = False
|
||||
async for event in restored_workflow.send_responses_streaming({
|
||||
request_info_event.request_id: True # Approve the request
|
||||
}):
|
||||
async for event in restored_workflow.run(
|
||||
stream=True,
|
||||
responses={
|
||||
request_info_event.request_id: True # Approve the request
|
||||
},
|
||||
):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
final_completed = True
|
||||
|
||||
|
||||
@@ -201,9 +201,11 @@ async def test_basic_sub_workflow() -> None:
|
||||
assert request_events[0].data.domain == "example.com"
|
||||
|
||||
# Send response through the main workflow
|
||||
await main_workflow.send_responses({
|
||||
request_events[0].request_id: True # Domain is approved
|
||||
})
|
||||
await main_workflow.run(
|
||||
responses={
|
||||
request_events[0].request_id: True # Domain is approved
|
||||
}
|
||||
)
|
||||
|
||||
# Check result
|
||||
assert parent.result is not None
|
||||
@@ -245,9 +247,11 @@ async def test_sub_workflow_with_interception():
|
||||
assert request_events[0].data.domain == "unknown.com"
|
||||
|
||||
# Send external response
|
||||
await main_workflow.send_responses({
|
||||
request_events[0].request_id: False # Domain not approved
|
||||
})
|
||||
await main_workflow.run(
|
||||
responses={
|
||||
request_events[0].request_id: False # Domain not approved
|
||||
}
|
||||
)
|
||||
assert parent.result is not None
|
||||
assert parent.result.email == "user@unknown.com"
|
||||
assert parent.result.is_valid is False
|
||||
@@ -447,7 +451,7 @@ async def test_concurrent_sub_workflow_execution() -> None:
|
||||
|
||||
# Send responses for all requests (approve all domains)
|
||||
responses = {event.request_id: True for event in request_events}
|
||||
await main_workflow.send_responses(responses)
|
||||
await main_workflow.run(responses=responses)
|
||||
|
||||
# All results should be collected
|
||||
assert len(processor.results) == len(emails)
|
||||
@@ -613,7 +617,7 @@ async def test_sub_workflow_checkpoint_restore_no_duplicate_requests() -> None:
|
||||
assert resumed_first_request_id == first_request_id
|
||||
|
||||
request_events: list[WorkflowEvent] = []
|
||||
async for event in workflow2.send_responses_streaming({resumed_first_request_id: "first_answer"}):
|
||||
async for event in workflow2.run(stream=True, responses={resumed_first_request_id: "first_answer"}):
|
||||
if event.type == "request_info":
|
||||
request_events.append(event)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from agent_framework._workflows._typing_utils import (
|
||||
normalize_type_to_list,
|
||||
resolve_type_annotation,
|
||||
serialize_type,
|
||||
try_coerce_to_type,
|
||||
)
|
||||
|
||||
# region: normalize_type_to_list tests
|
||||
@@ -420,3 +421,72 @@ def test_type_compatibility_complex() -> None:
|
||||
# Incompatible nested structure
|
||||
incompatible_target = list[dict[Union[str, bytes], int]]
|
||||
assert not is_type_compatible(source, incompatible_target)
|
||||
|
||||
|
||||
# region: try_coerce_to_type tests
|
||||
|
||||
|
||||
def test_coerce_already_correct_type() -> None:
|
||||
"""Values already matching the target type are returned as-is."""
|
||||
assert try_coerce_to_type(42, int) == 42
|
||||
assert try_coerce_to_type("hello", str) == "hello"
|
||||
assert try_coerce_to_type(True, bool) is True
|
||||
|
||||
|
||||
def test_coerce_int_to_float() -> None:
|
||||
"""JSON integers should be coercible to float."""
|
||||
result = try_coerce_to_type(1, float)
|
||||
assert result == 1.0
|
||||
assert isinstance(result, float)
|
||||
|
||||
|
||||
def test_coerce_dict_to_dataclass() -> None:
|
||||
"""Dicts (from JSON) should be coercible to dataclasses."""
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
result = try_coerce_to_type({"x": 1, "y": 2}, Point)
|
||||
assert isinstance(result, Point)
|
||||
assert result.x == 1
|
||||
assert result.y == 2
|
||||
|
||||
|
||||
def test_coerce_dict_to_dataclass_bad_keys_returns_original() -> None:
|
||||
"""Dicts with wrong keys should return the original dict, not raise."""
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
original = {"a": 1, "b": 2}
|
||||
result = try_coerce_to_type(original, Point)
|
||||
assert result is original
|
||||
|
||||
|
||||
def test_coerce_non_concrete_target_returns_original() -> None:
|
||||
"""Union and other non-concrete types should return the original value."""
|
||||
result = try_coerce_to_type(42, int | str)
|
||||
assert result == 42
|
||||
|
||||
result = try_coerce_to_type({"x": 1}, Union[str, int])
|
||||
assert result == {"x": 1}
|
||||
|
||||
|
||||
def test_coerce_unrelated_types_returns_original() -> None:
|
||||
"""Coercion between unrelated types should return the original value."""
|
||||
assert try_coerce_to_type("hello", int) == "hello"
|
||||
assert try_coerce_to_type(3.14, str) == 3.14
|
||||
assert try_coerce_to_type([1, 2], dict) == [1, 2]
|
||||
|
||||
|
||||
def test_coerce_any_returns_original() -> None:
|
||||
"""Any target type should accept any value without coercion."""
|
||||
assert try_coerce_to_type(42, Any) == 42
|
||||
assert try_coerce_to_type({"k": "v"}, Any) == {"k": "v"}
|
||||
|
||||
|
||||
# endregion: try_coerce_to_type tests
|
||||
|
||||
@@ -383,7 +383,7 @@ async def test_workflow_run_stream_from_checkpoint_with_external_storage(
|
||||
try:
|
||||
events: list[WorkflowEvent] = []
|
||||
async for event in workflow_without_checkpointing.run(
|
||||
checkpoint_id=checkpoint_id, checkpoint_storage=storage
|
||||
checkpoint_id=checkpoint_id, checkpoint_storage=storage, stream=True
|
||||
):
|
||||
events.append(event)
|
||||
if len(events) >= 2: # Limit to avoid infinite loops
|
||||
@@ -952,11 +952,11 @@ async def test_workflow_run_parameter_validation(simple_executor: Executor) -> N
|
||||
pass
|
||||
|
||||
# Invalid: none of message or checkpoint_id
|
||||
with pytest.raises(ValueError, match="Must provide either"):
|
||||
with pytest.raises(ValueError, match="Must provide at least one of"):
|
||||
await workflow.run()
|
||||
|
||||
# Invalid: none of message or checkpoint_id (streaming)
|
||||
with pytest.raises(ValueError, match="Must provide either"):
|
||||
with pytest.raises(ValueError, match="Must provide at least one of"):
|
||||
async for _ in workflow.run(stream=True):
|
||||
pass
|
||||
|
||||
@@ -1174,8 +1174,8 @@ async def test_output_executors_filtering_with_fan_in() -> None:
|
||||
assert outputs[0] == 40
|
||||
|
||||
|
||||
async def test_output_executors_filtering_with_send_responses() -> None:
|
||||
"""Test output filtering works correctly with send_responses method."""
|
||||
async def test_output_executors_filtering_with_run_responses() -> None:
|
||||
"""Test output filtering works correctly with run(responses=...) method."""
|
||||
executor = MockExecutorRequestApproval(id="approval_executor")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).with_output_from([executor]).build()
|
||||
@@ -1189,7 +1189,7 @@ async def test_output_executors_filtering_with_send_responses() -> None:
|
||||
|
||||
# Send approval response
|
||||
responses = {request_events[0].request_id: ApprovalMessage(approved=True)}
|
||||
response_result = await workflow.send_responses(responses)
|
||||
response_result = await workflow.run(responses=responses)
|
||||
outputs = response_result.get_outputs()
|
||||
|
||||
# Output should be yielded since approval_executor is in output_executors
|
||||
@@ -1197,8 +1197,8 @@ async def test_output_executors_filtering_with_send_responses() -> None:
|
||||
assert outputs[0] == 42
|
||||
|
||||
|
||||
async def test_output_executors_filtering_with_send_responses_streaming() -> None:
|
||||
"""Test output filtering works correctly with send_responses_streaming method."""
|
||||
async def test_output_executors_filtering_with_run_responses_streaming() -> None:
|
||||
"""Test output filtering works correctly with run(responses=..., stream=True) method."""
|
||||
executor = MockExecutorRequestApproval(id="approval_executor")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
@@ -1218,7 +1218,7 @@ async def test_output_executors_filtering_with_send_responses_streaming() -> Non
|
||||
# Send approval response via streaming
|
||||
responses = {request_events[0].request_id: ApprovalMessage(approved=True)}
|
||||
output_events: list[WorkflowEvent] = []
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
async for event in workflow.run(responses=responses, stream=True):
|
||||
if event.type == "output":
|
||||
output_events.append(event)
|
||||
|
||||
|
||||
+1
-1
@@ -755,7 +755,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
When externalLoop.when is configured and evaluates to true after agent response,
|
||||
this method emits an ExternalInputRequest via ctx.request_info() and returns.
|
||||
The workflow will yield, and when the caller provides a response via
|
||||
send_responses_streaming(), the handle_external_input_response handler
|
||||
run(responses=..., stream=True), the handle_external_input_response handler
|
||||
will continue the loop.
|
||||
"""
|
||||
state = await self._ensure_state_initialized(ctx, trigger)
|
||||
|
||||
@@ -451,8 +451,6 @@ class AgentFrameworkExecutor:
|
||||
logger.info(f"Resuming workflow with HIL responses for {len(hil_responses)} request(s)")
|
||||
|
||||
# Unwrap primitive responses if they're wrapped in {response: value} format
|
||||
from ._utils import parse_input_for_type
|
||||
|
||||
unwrapped_responses = {}
|
||||
for request_id, response_value in hil_responses.items():
|
||||
if isinstance(response_value, dict) and "response" in response_value:
|
||||
@@ -461,62 +459,16 @@ class AgentFrameworkExecutor:
|
||||
|
||||
hil_responses = unwrapped_responses
|
||||
|
||||
# NOTE: Two-step approach for stateless HTTP (framework limitation):
|
||||
# 1. Restore checkpoint to load pending requests into workflow's in-memory state
|
||||
# 2. Then send responses using send_responses_streaming
|
||||
# Future: Framework should support run(stream=True, checkpoint_id, responses) in single call
|
||||
# (checkpoint_id is guaranteed to exist due to earlier validation)
|
||||
logger.debug(f"Restoring checkpoint {checkpoint_id} then sending HIL responses")
|
||||
logger.debug(f"Restoring checkpoint {checkpoint_id} and sending HIL responses")
|
||||
|
||||
try:
|
||||
# Step 1: Restore checkpoint to populate workflow's in-memory pending requests
|
||||
restored = False
|
||||
async for _event in workflow.run(
|
||||
async for event in workflow.run(
|
||||
stream=True,
|
||||
responses=hil_responses,
|
||||
checkpoint_id=checkpoint_id,
|
||||
checkpoint_storage=checkpoint_storage,
|
||||
):
|
||||
restored = True
|
||||
break # Stop immediately after restoration, don't process events
|
||||
|
||||
if not restored:
|
||||
raise RuntimeError("Checkpoint restoration did not yield any events")
|
||||
|
||||
# Reset running flags so we can call send_responses_streaming
|
||||
if hasattr(workflow, "_is_running"):
|
||||
workflow._is_running = False
|
||||
if hasattr(workflow, "_runner") and hasattr(workflow._runner, "_running"):
|
||||
workflow._runner._running = False
|
||||
|
||||
# Extract response types from restored workflow and convert responses to proper types
|
||||
try:
|
||||
if hasattr(workflow, "_runner") and hasattr(workflow._runner, "context"):
|
||||
runner_context = workflow._runner.context
|
||||
pending_requests_dict = await runner_context.get_pending_request_info_events()
|
||||
|
||||
converted_responses = {}
|
||||
for request_id, response_value in hil_responses.items():
|
||||
if request_id in pending_requests_dict:
|
||||
pending_request = pending_requests_dict[request_id]
|
||||
if hasattr(pending_request, "response_type"):
|
||||
response_type = pending_request.response_type
|
||||
try:
|
||||
response_value = parse_input_for_type(response_value, response_type)
|
||||
logger.debug(
|
||||
f"Converted HIL response for {request_id} to {type(response_value)}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to convert HIL response for {request_id}: {e}")
|
||||
|
||||
converted_responses[request_id] = response_value
|
||||
|
||||
hil_responses = converted_responses
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not convert HIL responses to proper types: {e}")
|
||||
|
||||
async for event in workflow.send_responses_streaming(hil_responses):
|
||||
# Enrich new request_info events (type='request_info')
|
||||
# that may come from subsequent HIL requests
|
||||
# Enrich new request_info events that may come from subsequent HIL requests
|
||||
if event.type == "request_info":
|
||||
self._enrich_request_info_event_with_response_schema(event, workflow)
|
||||
|
||||
|
||||
@@ -1524,7 +1524,7 @@ class MagenticBuilder:
|
||||
if request.kind == MagenticHumanInterventionKind.PLAN_REVIEW:
|
||||
# Review plan and respond
|
||||
reply = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.APPROVE)
|
||||
await workflow.send_responses({event.request_id: reply})
|
||||
await workflow.run(responses={event.request_id: reply})
|
||||
|
||||
See Also:
|
||||
- :class:`MagenticHumanInterventionRequest`: Event emitted for review
|
||||
|
||||
@@ -784,9 +784,9 @@ async def test_group_chat_with_request_info_filtering():
|
||||
|
||||
# Continue the workflow with a response
|
||||
outputs: list[WorkflowEvent] = []
|
||||
async for event in workflow.send_responses_streaming({
|
||||
request_event.request_id: AgentRequestInfoResponse.approve()
|
||||
}):
|
||||
async for event in workflow.run(
|
||||
stream=True, responses={request_event.request_id: AgentRequestInfoResponse.approve()}
|
||||
):
|
||||
if event.type == "output":
|
||||
outputs.append(event)
|
||||
|
||||
|
||||
@@ -255,9 +255,9 @@ async def test_handoff_async_termination_condition() -> None:
|
||||
assert requests
|
||||
|
||||
events = await _drain(
|
||||
workflow.send_responses_streaming({
|
||||
requests[-1].request_id: [ChatMessage(role="user", text="Second user message")]
|
||||
})
|
||||
workflow.run(
|
||||
stream=True, responses={requests[-1].request_id: [ChatMessage(role="user", text="Second user message")]}
|
||||
)
|
||||
)
|
||||
outputs = [ev for ev in events if ev.type == "output"]
|
||||
assert len(outputs) == 1
|
||||
@@ -508,7 +508,7 @@ async def test_handoff_with_participant_factories():
|
||||
|
||||
# Follow-up message
|
||||
events = await _drain(
|
||||
workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage(role="user", text="More details")]})
|
||||
workflow.run(stream=True, responses={requests[-1].request_id: [ChatMessage(role="user", text="More details")]})
|
||||
)
|
||||
outputs = [ev for ev in events if ev.type == "output"]
|
||||
assert outputs
|
||||
@@ -582,7 +582,9 @@ async def test_handoff_with_participant_factories_and_add_handoff():
|
||||
|
||||
# Second user message - specialist_a hands off to specialist_b
|
||||
events = await _drain(
|
||||
workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage(role="user", text="Need escalation")]})
|
||||
workflow.run(
|
||||
stream=True, responses={requests[-1].request_id: [ChatMessage(role="user", text="Need escalation")]}
|
||||
)
|
||||
)
|
||||
requests = [ev for ev in events if ev.type == "request_info"]
|
||||
assert requests
|
||||
@@ -617,7 +619,7 @@ async def test_handoff_participant_factories_with_checkpointing():
|
||||
assert requests
|
||||
|
||||
events = await _drain(
|
||||
workflow.send_responses_streaming({requests[-1].request_id: [ChatMessage(role="user", text="follow up")]})
|
||||
workflow.run(stream=True, responses={requests[-1].request_id: [ChatMessage(role="user", text="follow up")]})
|
||||
)
|
||||
outputs = [ev for ev in events if ev.type == "output"]
|
||||
assert outputs, "Should have workflow output after termination condition is met"
|
||||
|
||||
@@ -251,7 +251,7 @@ async def test_magentic_workflow_plan_review_approval_to_completion():
|
||||
|
||||
completed = False
|
||||
output: list[ChatMessage] | None = None
|
||||
async for ev in wf.send_responses_streaming(responses={req_event.request_id: req_event.data.approve()}):
|
||||
async for ev in wf.run(stream=True, responses={req_event.request_id: req_event.data.approve()}):
|
||||
if ev.type == "status" and ev.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
elif ev.type == "output":
|
||||
@@ -297,16 +297,17 @@ async def test_magentic_plan_review_with_revise():
|
||||
# Send a revise response
|
||||
saw_second_review = False
|
||||
completed = False
|
||||
async for ev in wf.send_responses_streaming(
|
||||
responses={req_event.request_id: req_event.data.revise("Looks good; consider Z")}
|
||||
async for ev in wf.run(
|
||||
stream=True, responses={req_event.request_id: req_event.data.revise("Looks good; consider Z")}
|
||||
):
|
||||
if ev.type == "request_info" and ev.request_type is MagenticPlanReviewRequest:
|
||||
saw_second_review = True
|
||||
req_event = ev
|
||||
|
||||
# Approve the second review
|
||||
async for ev in wf.send_responses_streaming(
|
||||
responses={req_event.request_id: req_event.data.approve()} # type: ignore[union-attr]
|
||||
async for ev in wf.run(
|
||||
stream=True,
|
||||
responses={req_event.request_id: req_event.data.approve()}, # type: ignore[union-attr]
|
||||
):
|
||||
if ev.type == "status" and ev.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
@@ -397,7 +398,7 @@ async def test_magentic_checkpoint_resume_round_trip():
|
||||
assert isinstance(req_event.data, MagenticPlanReviewRequest)
|
||||
|
||||
responses = {req_event.request_id: req_event.data.approve()}
|
||||
async for event in wf_resume.send_responses_streaming(responses=responses):
|
||||
async for event in wf_resume.run(stream=True, responses=responses):
|
||||
if event.type == "output":
|
||||
completed = event
|
||||
assert completed is not None
|
||||
|
||||
@@ -193,7 +193,7 @@ async def run_agent_framework() -> None:
|
||||
current_executor = None
|
||||
stream_line_open = False
|
||||
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
async for event in workflow.run(stream=True, responses=responses):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
# Print executor name header when switching to a new agent
|
||||
if current_executor != event.executor_id:
|
||||
|
||||
@@ -208,9 +208,9 @@ async def _run_workflow(workflow: Workflow, user_inputs: list[str]) -> None:
|
||||
responses = {req.request_id: HandoffAgentUserRequest.terminate() for req in pending_requests}
|
||||
|
||||
# Send responses and get new events
|
||||
# We use send_responses_streaming() to get events as they occur, allowing us to
|
||||
# display agent responses in real-time and handle new requests as they arrive
|
||||
workflow_result = await workflow.send_responses(responses)
|
||||
# We use run(responses=...) to get events, allowing us to
|
||||
# display agent responses and handle new requests as they arrive
|
||||
workflow_result = await workflow.run(responses=responses)
|
||||
pending_requests = _handle_events(workflow_result)
|
||||
|
||||
|
||||
|
||||
@@ -255,9 +255,9 @@ async def main() -> None:
|
||||
}
|
||||
|
||||
# Send responses and get new events
|
||||
# We use send_responses() to get events from the workflow, allowing us to
|
||||
# We use run(responses=...) to get events from the workflow, allowing us to
|
||||
# display agent responses and handle new requests as they arrive
|
||||
events = await workflow.send_responses(responses)
|
||||
events = await workflow.run(responses=responses)
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
"""
|
||||
|
||||
@@ -191,7 +191,7 @@ async def main() -> None:
|
||||
print(f"\nUser: {user_input}")
|
||||
|
||||
responses = {request.request_id: HandoffAgentUserRequest.create_response(user_input)}
|
||||
events = await _drain(workflow.send_responses_streaming(responses))
|
||||
events = await _drain(workflow.run(stream=True, responses=responses))
|
||||
requests, file_ids = _handle_events(events)
|
||||
all_file_ids.extend(file_ids)
|
||||
input_index += 1
|
||||
|
||||
@@ -29,7 +29,7 @@ Concepts highlighted here:
|
||||
must keep stable IDs so the checkpoint state aligns when we rebuild the graph.
|
||||
2. **Executor snapshotting** - checkpoints capture the pending plan-review request
|
||||
map, at superstep boundaries.
|
||||
3. **Resume with responses** - `Workflow.send_responses_streaming` accepts a
|
||||
3. **Resume with responses** - `Workflow.run(responses=...)` accepts a
|
||||
`responses` mapping so we can inject the stored human reply during restoration.
|
||||
|
||||
Prerequisites:
|
||||
@@ -157,7 +157,7 @@ async def main() -> None:
|
||||
|
||||
# Supply the approval and continue to run to completion.
|
||||
final_event: WorkflowEvent | None = None
|
||||
async for event in resumed_workflow.send_responses_streaming({request_info_event.request_id: approval}):
|
||||
async for event in resumed_workflow.run(stream=True, responses={request_info_event.request_id: approval}):
|
||||
if event.type == "output":
|
||||
final_event = event
|
||||
|
||||
|
||||
@@ -139,14 +139,14 @@ async def main() -> None:
|
||||
print("=" * 60)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run(task, stream=True)
|
||||
|
||||
pending_responses = await process_event_stream(stream)
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -281,7 +281,7 @@ async def main() -> None:
|
||||
)
|
||||
initial_run = False
|
||||
elif pending_responses is not None:
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = None
|
||||
else:
|
||||
break
|
||||
|
||||
+1
-1
@@ -249,7 +249,7 @@ async def run_interactive_session(
|
||||
|
||||
while True:
|
||||
if responses:
|
||||
event_stream = workflow.send_responses_streaming(responses)
|
||||
event_stream = workflow.run(stream=True, responses=responses)
|
||||
requests.clear()
|
||||
responses = None
|
||||
else:
|
||||
|
||||
+19
-18
@@ -23,22 +23,24 @@ from azure.identity import AzureCliCredential
|
||||
"""
|
||||
Sample: Handoff Workflow with Tool Approvals + Checkpoint Resume
|
||||
|
||||
Demonstrates the two-step pattern for resuming a handoff workflow from a checkpoint
|
||||
while handling both HandoffAgentUserRequest prompts and function approval request Content
|
||||
for tool calls (e.g., submit_refund).
|
||||
Demonstrates resuming a handoff workflow from a checkpoint while handling both
|
||||
HandoffAgentUserRequest prompts and function approval request Content for tool calls
|
||||
(e.g., submit_refund).
|
||||
|
||||
Scenario:
|
||||
1. User starts a conversation with the workflow.
|
||||
2. Agents may emit user input requests or tool approval requests.
|
||||
3. Workflow writes a checkpoint capturing pending requests and pauses.
|
||||
4. Process can exit/restart.
|
||||
5. On resume: Load the checkpoint, surface pending approvals/user prompts, and provide responses.
|
||||
5. On resume: Restore checkpoint, inspect pending requests, then provide responses.
|
||||
6. Workflow continues from the saved state.
|
||||
|
||||
Pattern:
|
||||
- Step 1: workflow.run(checkpoint_id=..., stream=True) to restore checkpoint and pending requests.
|
||||
- Step 2: workflow.send_responses_streaming(responses) to supply human replies and approvals.
|
||||
- Two-step approach is required because send_responses_streaming does not accept checkpoint_id.
|
||||
- workflow.run(checkpoint_id=..., stream=True) to restore checkpoint and discover pending requests.
|
||||
- workflow.run(stream=True, responses=responses) to supply human replies and approvals.
|
||||
(Two steps are needed here because the sample must inspect request types before building responses.
|
||||
When response payloads are already known, use the single-call form:
|
||||
workflow.run(stream=True, checkpoint_id=..., responses=responses).)
|
||||
|
||||
Prerequisites:
|
||||
- Azure CLI authentication (az login).
|
||||
@@ -228,13 +230,13 @@ async def resume_with_responses(
|
||||
approve_tools: bool | None = None,
|
||||
) -> tuple[list[WorkflowEvent], str | None]:
|
||||
"""
|
||||
Two-step resume pattern (answers customer questions and tool approvals):
|
||||
Resume from checkpoint and send responses.
|
||||
|
||||
Step 1: Restore checkpoint to load pending requests into workflow state
|
||||
Step 2: Send user responses using send_responses_streaming
|
||||
Step 1: Restore checkpoint to discover pending request types.
|
||||
Step 2: Build typed responses and send via workflow.run(responses=...).
|
||||
|
||||
This is the current pattern required because send_responses_streaming
|
||||
doesn't accept a checkpoint_id parameter.
|
||||
When response payloads are already known, these can be combined into a single
|
||||
workflow.run(stream=True, checkpoint_id=..., responses=...) call.
|
||||
"""
|
||||
print(f"\n{'=' * 60}")
|
||||
print("RESUMING WORKFLOW WITH HUMAN INPUT")
|
||||
@@ -253,10 +255,9 @@ async def resume_with_responses(
|
||||
checkpoints.sort(key=lambda cp: cp.timestamp, reverse=True)
|
||||
latest_checkpoint = checkpoints[0]
|
||||
|
||||
print(f"Step 1: Restoring checkpoint {latest_checkpoint.checkpoint_id}")
|
||||
print(f"Restoring checkpoint {latest_checkpoint.checkpoint_id}")
|
||||
|
||||
# Step 1: Restore the checkpoint to load pending requests into memory
|
||||
# The checkpoint restoration re-emits pending request_info events
|
||||
# First, restore checkpoint to discover pending requests
|
||||
restored_requests: list[WorkflowEvent] = []
|
||||
async for event in workflow.run(checkpoint_id=latest_checkpoint.checkpoint_id, stream=True): # type: ignore[attr-defined]
|
||||
if event.type == "request_info":
|
||||
@@ -274,11 +275,11 @@ async def resume_with_responses(
|
||||
user_response=user_response,
|
||||
approve_tools=approve_tools,
|
||||
)
|
||||
print(f"Step 2: Sending responses for {len(responses)} request(s)")
|
||||
print(f"Sending responses for {len(responses)} request(s)")
|
||||
|
||||
new_pending_requests: list[WorkflowEvent] = []
|
||||
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
async for event in workflow.run(stream=True, responses=responses):
|
||||
if event.type == "status":
|
||||
print(f"[Status] {event.state}")
|
||||
|
||||
@@ -309,7 +310,7 @@ async def main() -> None:
|
||||
This sample shows:
|
||||
1. Starting a workflow and getting a HandoffAgentUserRequest
|
||||
2. Pausing (checkpoint is saved automatically)
|
||||
3. Resuming from checkpoint with a user response or tool approval (two-step pattern)
|
||||
3. Resuming from checkpoint with a user response or tool approval
|
||||
4. Continuing the conversation until completion
|
||||
"""
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ async def main() -> None:
|
||||
|
||||
approval_response = "approve"
|
||||
output_event: WorkflowEvent | None = None
|
||||
async for event in workflow2.send_responses_streaming({request_info_event.request_id: approval_response}):
|
||||
async for event in workflow2.run(stream=True, responses={request_info_event.request_id: approval_response}):
|
||||
if event.type == "output":
|
||||
output_event = event
|
||||
|
||||
|
||||
+1
-1
@@ -347,7 +347,7 @@ async def main() -> None:
|
||||
else:
|
||||
print(f"Unknown request info event data type: {type(event.data)}")
|
||||
|
||||
run_result = await main_workflow.send_responses(responses)
|
||||
run_result = await main_workflow.run(responses=responses)
|
||||
|
||||
outputs = run_result.get_outputs()
|
||||
if outputs:
|
||||
|
||||
@@ -251,7 +251,7 @@ async def main() -> None:
|
||||
# Continue workflow with user response
|
||||
print(f"\n{YELLOW}WORKFLOW:{RESET} Restore\n")
|
||||
response = AgentExternalInputResponse(user_input=user_input)
|
||||
stream = workflow.send_responses_streaming({pending_request_id: response})
|
||||
stream = workflow.run(stream=True, responses={pending_request_id: response})
|
||||
pending_request_id = None
|
||||
else:
|
||||
# Start workflow
|
||||
|
||||
@@ -90,7 +90,7 @@ async def main():
|
||||
while True:
|
||||
if pending_request_id:
|
||||
response = ExternalInputResponse(user_input=user_input)
|
||||
stream = workflow.send_responses_streaming({pending_request_id: response})
|
||||
stream = workflow.run(stream=True, responses={pending_request_id: response})
|
||||
else:
|
||||
stream = workflow.run({"userInput": user_input}, stream=True)
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run(
|
||||
"Create a short launch blurb for the LumenX desk lamp. Emphasize adjustability and warm lighting.",
|
||||
stream=True,
|
||||
@@ -209,7 +209,7 @@ async def main() -> None:
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
print("\nWorkflow complete.")
|
||||
|
||||
+2
-2
@@ -249,7 +249,7 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
events = await workflow.run(incoming_email)
|
||||
request_info_events = events.get_request_info_events()
|
||||
|
||||
@@ -276,7 +276,7 @@ async def main() -> None:
|
||||
print("Performing automatic approval for demo purposes...")
|
||||
responses[request_info_event.request_id] = data.to_function_approval_response(approved=True)
|
||||
|
||||
events = await workflow.send_responses(responses)
|
||||
events = await workflow.run(responses=responses)
|
||||
request_info_events = events.get_request_info_events()
|
||||
|
||||
# The output should only come from conclude_workflow executor and it's a single string
|
||||
|
||||
+2
-2
@@ -183,14 +183,14 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run("Analyze the impact of large language models on software development.", stream=True)
|
||||
|
||||
pending_responses = await process_event_stream(stream)
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -147,7 +147,7 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run(
|
||||
"Discuss how our team should approach adopting AI tools for productivity. "
|
||||
"Consider benefits, risks, and implementation strategies.",
|
||||
@@ -158,7 +158,7 @@ async def main() -> None:
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
|
||||
|
||||
+5
-5
@@ -29,7 +29,7 @@ the workflow completes when idle with no pending work.
|
||||
|
||||
Purpose:
|
||||
Show how to integrate a human step in the middle of an LLM workflow by using
|
||||
`request_info` and `send_responses_streaming`.
|
||||
`request_info` and `run(responses=..., stream=True)`.
|
||||
|
||||
Demonstrate:
|
||||
- Alternating turns between an AgentExecutor and a human, driven by events.
|
||||
@@ -42,11 +42,11 @@ Prerequisites:
|
||||
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs.
|
||||
"""
|
||||
|
||||
# How human-in-the-loop is achieved via `request_info` and `send_responses_streaming`:
|
||||
# How human-in-the-loop is achieved via `request_info` and `run(responses=..., stream=True)`:
|
||||
# - An executor (TurnManager) calls `ctx.request_info` with a payload (HumanFeedbackRequest).
|
||||
# - The workflow run pauses and emits a with the payload and the request_id.
|
||||
# - The application captures the event, prompts the user, and collects replies.
|
||||
# - The application calls `send_responses_streaming` with a map of request_ids to replies.
|
||||
# - The application calls `run(stream=True, responses=...)` with a map of request_ids to replies.
|
||||
# - The workflow resumes, and the response is delivered to the executor method decorated with @response_handler.
|
||||
# - The executor can then continue the workflow, e.g., by sending a new message to the agent.
|
||||
|
||||
@@ -205,14 +205,14 @@ async def main() -> None:
|
||||
).build()
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run("start", stream=True)
|
||||
|
||||
pending_responses = await process_event_stream(stream)
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
"""
|
||||
|
||||
+4
-4
@@ -13,8 +13,8 @@ using the standard request_info pattern for consistency.
|
||||
|
||||
Demonstrate:
|
||||
- Configuring request info with `.with_request_info()`
|
||||
- Handling with AgentInputRequest data
|
||||
- Injecting responses back into the workflow via send_responses_streaming
|
||||
- Handling request_info events with AgentInputRequest data
|
||||
- Injecting responses back into the workflow via run(responses=..., stream=True)
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables
|
||||
@@ -122,14 +122,14 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run("Write a brief introduction to artificial intelligence.", stream=True)
|
||||
|
||||
pending_responses = await process_event_stream(stream)
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
AgentRunUpdateEvent,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
MagenticBuilder,
|
||||
MagenticPlanReviewRequest,
|
||||
WorkflowEvent,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
Sample: Magentic Orchestration with Human Plan Review
|
||||
|
||||
This sample demonstrates how humans can review and provide feedback on plans
|
||||
generated by the Magentic workflow orchestrator. When plan review is enabled,
|
||||
the workflow requests human approval or revision before executing each plan.
|
||||
|
||||
Key concepts:
|
||||
- with_plan_review(): Enables human review of generated plans
|
||||
- MagenticPlanReviewRequest: The event type for plan review requests
|
||||
- Human can choose to: approve the plan or provide revision feedback
|
||||
|
||||
Plan review options:
|
||||
- approve(): Accept the proposed plan and continue execution
|
||||
- revise(feedback): Provide textual feedback to modify the plan
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI credentials configured for `OpenAIChatClient`.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
researcher_agent = ChatAgent(
|
||||
name="ResearcherAgent",
|
||||
description="Specialist in research and information gathering",
|
||||
instructions="You are a Researcher. You find information and gather facts.",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
)
|
||||
|
||||
analyst_agent = ChatAgent(
|
||||
name="AnalystAgent",
|
||||
description="Data analyst who processes and summarizes research findings",
|
||||
instructions="You are an Analyst. You analyze findings and create summaries.",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
)
|
||||
|
||||
manager_agent = ChatAgent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the workflow",
|
||||
instructions="You coordinate a team to complete tasks efficiently.",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
)
|
||||
|
||||
print("\nBuilding Magentic Workflow with Human Plan Review...")
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([researcher_agent, analyst_agent])
|
||||
.with_manager(
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=1,
|
||||
max_reset_count=2,
|
||||
)
|
||||
.with_plan_review() # Request human input for plan review
|
||||
.build()
|
||||
)
|
||||
|
||||
task = "Research sustainable aviation fuel technology and summarize the findings."
|
||||
|
||||
print(f"\nTask: {task}")
|
||||
print("\nStarting workflow execution...")
|
||||
print("=" * 60)
|
||||
|
||||
pending_request: WorkflowEvent | None = None
|
||||
pending_responses: dict[str, object] | None = None
|
||||
output_event: WorkflowEvent | None = None
|
||||
|
||||
while not output_event:
|
||||
if pending_responses is not None:
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
else:
|
||||
stream = workflow.run(task, stream=True)
|
||||
|
||||
last_message_id: str | None = None
|
||||
async for event in stream:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
message_id = event.data.message_id
|
||||
if message_id != last_message_id:
|
||||
if last_message_id is not None:
|
||||
print("\n")
|
||||
print(f"- {event.executor_id}:", end=" ", flush=True)
|
||||
last_message_id = message_id
|
||||
print(event.data, end="", flush=True)
|
||||
|
||||
elif event.type == "request_info" and event.request_type is MagenticPlanReviewRequest:
|
||||
pending_request = event
|
||||
|
||||
elif event.type == "output":
|
||||
output_event = event
|
||||
|
||||
pending_responses = None
|
||||
|
||||
# Handle plan review request if any
|
||||
if pending_request is not None:
|
||||
event_data = cast(MagenticPlanReviewRequest, pending_request.data)
|
||||
|
||||
print("\n\n[Magentic Plan Review Request]")
|
||||
if event_data.current_progress is not None:
|
||||
print("Current Progress Ledger:")
|
||||
print(json.dumps(event_data.current_progress.to_dict(), indent=2))
|
||||
print()
|
||||
print(f"Proposed Plan:\n{event_data.plan.text}\n")
|
||||
print("Please provide your feedback (press Enter to approve):")
|
||||
|
||||
reply = await asyncio.get_event_loop().run_in_executor(None, input, "> ")
|
||||
if reply.strip() == "":
|
||||
print("Plan approved.\n")
|
||||
pending_responses = {pending_request.request_id: event_data.approve()}
|
||||
else:
|
||||
print("Plan revised by human.\n")
|
||||
pending_responses = {pending_request.request_id: event_data.revise(reply)}
|
||||
pending_request = None
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("WORKFLOW COMPLETED")
|
||||
print("=" * 60)
|
||||
print("Final Output:")
|
||||
# The output of the Magentic workflow is a list of ChatMessages with only one final message
|
||||
# generated by the orchestrator.
|
||||
output_messages = cast(list[ChatMessage], output_event.data)
|
||||
if output_messages:
|
||||
output = output_messages[-1].text
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+2
-2
@@ -155,7 +155,7 @@ async def main() -> None:
|
||||
print("-" * 60)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run(
|
||||
"Manage my portfolio. Use a max of 5000 dollars to adjust my position using "
|
||||
"your best judgment based on market sentiment. No need to confirm trades with me.",
|
||||
@@ -166,7 +166,7 @@ async def main() -> None:
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
"""
|
||||
|
||||
+2
-2
@@ -165,7 +165,7 @@ async def main() -> None:
|
||||
print("-" * 60)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run(
|
||||
"We need to deploy version 2.4.0 to production. Please coordinate the deployment.", stream=True
|
||||
)
|
||||
@@ -174,7 +174,7 @@ async def main() -> None:
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
"""
|
||||
|
||||
+4
-4
@@ -34,8 +34,8 @@ requiring any additional builder configuration.
|
||||
|
||||
Demonstrate:
|
||||
- Using @tool(approval_mode="always_require") for sensitive operations.
|
||||
- Handling with function_approval_request Content in sequential workflows.
|
||||
- Resuming workflow execution after approval via send_responses_streaming.
|
||||
- Handling request_info events with function_approval_request Content in sequential workflows.
|
||||
- Resuming workflow execution after approval via run(responses=..., stream=True).
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI or Azure OpenAI configured with the required environment variables.
|
||||
@@ -118,7 +118,7 @@ async def main() -> None:
|
||||
print("-" * 60)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run(
|
||||
"Check the schema and then update all orders with status 'pending' to 'processing'", stream=True
|
||||
)
|
||||
@@ -127,7 +127,7 @@ async def main() -> None:
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
"""
|
||||
|
||||
@@ -252,7 +252,7 @@ async def run_agent_framework_example(initial_task: str, scripted_responses: Seq
|
||||
except StopIteration:
|
||||
user_reply = "Thanks, that's all."
|
||||
responses = {request.request_id: user_reply for request in pending}
|
||||
final_events = await _drain_events(workflow.send_responses_streaming(responses))
|
||||
final_events = await _drain_events(workflow.run(stream=True, responses=responses))
|
||||
pending = _collect_handoff_requests(final_events)
|
||||
|
||||
conversation = _extract_final_conversation(final_events)
|
||||
|
||||
Reference in New Issue
Block a user