mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [Breaking] Remove WorkflowCompletedEvent, introduce workflow output and migrate to ctx.yield_output() + a huge refactoring (#845)
* Introduce input and output types for executor and workflow * WorkflowOutputContext handles two types * Remove can_handle_types from Executor * Update validation * Move workflow executor * Move workflow executor * Fix issues in WorkflowExecutor * refactor executor * update execute signature to create workflow context within Executor * fix simple sub workflow test; fix validation * fix output types in WorkflowExecutor * fix issue in Executor handling of SubWorkflowRequestInfo * update tests to use proper workflow output * update orchestration patterns to use output * Update sample -- not finished * Update python/packages/main/tests/workflow/test_workflow_states.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/packages/main/tests/workflow/test_concurrent.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * address comments * WorkflowOutputContext --> WorkflowContext * remove WorkflowCompletedEvent * update samples * Update doc string for important classes; update WorkflowExecutor to support concurrent execution * use Never instead of None for default type * Update usage of WorkflowContext[None to WorkflowContext[Never * address comments * remove filter for None * address comments, minor fixes * quality of life improvement on interceptor types --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
0f913bcdeb
commit
2133043f11
@@ -4,6 +4,8 @@ import asyncio
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import ( # Core chat primitives used to build requests
|
||||
AgentExecutor, # Wraps an LLM agent that can be invoked inside a workflow
|
||||
AgentExecutorRequest, # Input message bundle for an AgentExecutor
|
||||
@@ -11,7 +13,6 @@ from agent_framework import ( # Core chat primitives used to build requests
|
||||
ChatMessage,
|
||||
Role,
|
||||
WorkflowBuilder, # Fluent builder for wiring executors and edges
|
||||
WorkflowCompletedEvent, # Event we emit at the end to signal completion
|
||||
WorkflowContext, # Per-run context and event bus
|
||||
executor, # Decorator to declare a Python function as a workflow executor
|
||||
)
|
||||
@@ -41,15 +42,16 @@ and have the Azure OpenAI environment variables set as documented in the getting
|
||||
High level flow:
|
||||
1) spam_detection_agent reads an email and returns DetectionResult.
|
||||
2) If not spam, we transform the detection output into a user message for email_assistant_agent, then finish by
|
||||
sending the drafted reply.
|
||||
3) If spam, we short circuit to a spam handler that emits a completion event.
|
||||
yielding the drafted reply as workflow output.
|
||||
3) If spam, we short circuit to a spam handler that yields a spam notice as workflow output.
|
||||
|
||||
Output:
|
||||
- The final WorkflowCompletedEvent is printed to stdout, either with a drafted reply or a spam notice.
|
||||
- The final workflow output is printed to stdout, either with a drafted reply or a spam notice.
|
||||
|
||||
Notes:
|
||||
- Conditions read the agent response text and validate it into DetectionResult for robust routing.
|
||||
- Executors are small and single purpose to keep control flow easy to follow.
|
||||
- The workflow completes when it becomes idle, not via explicit completion events.
|
||||
"""
|
||||
|
||||
|
||||
@@ -96,18 +98,18 @@ def get_condition(expected_result: bool):
|
||||
|
||||
|
||||
@executor(id="send_email")
|
||||
async def handle_email_response(response: AgentExecutorResponse, ctx: WorkflowContext[None]) -> None:
|
||||
# Downstream of the email assistant. Parse a validated EmailResponse and emit a completion event.
|
||||
async def handle_email_response(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
# Downstream of the email assistant. Parse a validated EmailResponse and yield the workflow output.
|
||||
email_response = EmailResponse.model_validate_json(response.agent_run_response.text)
|
||||
await ctx.add_event(WorkflowCompletedEvent(f"Email sent:\n{email_response.response}"))
|
||||
await ctx.yield_output(f"Email sent:\n{email_response.response}")
|
||||
|
||||
|
||||
@executor(id="handle_spam")
|
||||
async def handle_spam_classifier_response(response: AgentExecutorResponse, ctx: WorkflowContext[None]) -> None:
|
||||
# Spam path. Confirm the DetectionResult and finish with the reason. Guard against accidental non spam input.
|
||||
async def handle_spam_classifier_response(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
# Spam path. Confirm the DetectionResult and yield the workflow output. Guard against accidental non spam input.
|
||||
detection = DetectionResult.model_validate_json(response.agent_run_response.text)
|
||||
if detection.is_spam:
|
||||
await ctx.add_event(WorkflowCompletedEvent(f"Email marked as spam: {detection.reason}"))
|
||||
await ctx.yield_output(f"Email marked as spam: {detection.reason}")
|
||||
else:
|
||||
# This indicates the routing predicate and executor contract are out of sync.
|
||||
raise RuntimeError("This executor should only handle spam messages.")
|
||||
@@ -184,11 +186,12 @@ async def main() -> None:
|
||||
email = email_file.read()
|
||||
|
||||
# Execute the workflow. Since the start is an AgentExecutor, pass an AgentExecutorRequest.
|
||||
# run_stream yields events as they occur. We watch for the terminal WorkflowCompletedEvent and print it.
|
||||
# The workflow completes when it becomes idle (no more work to do).
|
||||
request = AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email)], should_respond=True)
|
||||
async for event in workflow.run_stream(request):
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
print(f"{event}")
|
||||
events = await workflow.run(request)
|
||||
outputs = events.get_outputs()
|
||||
if outputs:
|
||||
print(f"Workflow output: {outputs[0]}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
@@ -214,7 +217,7 @@ async def main() -> None:
|
||||
(555) 123-4567
|
||||
----------------------------------------
|
||||
|
||||
WorkflowCompletedEvent(data=Email sent:
|
||||
Workflow output: Email sent:
|
||||
Hi Alex,
|
||||
|
||||
Thank you for the follow-up and for summarizing the action items from this morning's meeting. The points you listed accurately reflect our discussion, and I don't have any additional items to add at this time.
|
||||
@@ -224,7 +227,7 @@ async def main() -> None:
|
||||
Thank you again for outlining the next steps.
|
||||
|
||||
Best regards,
|
||||
Sarah)
|
||||
Sarah
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
|
||||
+19
-16
@@ -8,6 +8,8 @@ from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutor,
|
||||
AgentExecutorRequest,
|
||||
@@ -15,9 +17,9 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.azure import AzureChatClient
|
||||
@@ -30,7 +32,7 @@ Sample: Multi-Selection Edge Group for email triage and response.
|
||||
The workflow stores an email,
|
||||
classifies it as NotSpam, Spam, or Uncertain, and then routes to one or more branches.
|
||||
Non-spam emails are drafted into replies, long ones are also summarized, spam is blocked, and uncertain cases are
|
||||
flagged. Each path ends with simulated database persistence.
|
||||
flagged. Each path ends with simulated database persistence. The workflow completes when it becomes idle.
|
||||
|
||||
Purpose:
|
||||
Demonstrate how to use a multi-selection edge group to fan out from one executor to multiple possible targets.
|
||||
@@ -123,9 +125,9 @@ async def submit_to_email_assistant(analysis: AnalysisResult, ctx: WorkflowConte
|
||||
|
||||
|
||||
@executor(id="finalize_and_send")
|
||||
async def finalize_and_send(response: AgentExecutorResponse, ctx: WorkflowContext[None]) -> None:
|
||||
async def finalize_and_send(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
parsed = EmailResponse.model_validate_json(response.agent_run_response.text)
|
||||
await ctx.add_event(WorkflowCompletedEvent(f"Email sent: {parsed.response}"))
|
||||
await ctx.yield_output(f"Email sent: {parsed.response}")
|
||||
|
||||
|
||||
@executor(id="summarize_email")
|
||||
@@ -155,28 +157,26 @@ async def merge_summary(response: AgentExecutorResponse, ctx: WorkflowContext[An
|
||||
|
||||
|
||||
@executor(id="handle_spam")
|
||||
async def handle_spam(analysis: AnalysisResult, ctx: WorkflowContext[None]) -> None:
|
||||
async def handle_spam(analysis: AnalysisResult, ctx: WorkflowContext[Never, str]) -> None:
|
||||
if analysis.spam_decision == "Spam":
|
||||
await ctx.add_event(WorkflowCompletedEvent(f"Email marked as spam: {analysis.reason}"))
|
||||
await ctx.yield_output(f"Email marked as spam: {analysis.reason}")
|
||||
else:
|
||||
raise RuntimeError("This executor should only handle Spam messages.")
|
||||
|
||||
|
||||
@executor(id="handle_uncertain")
|
||||
async def handle_uncertain(analysis: AnalysisResult, ctx: WorkflowContext[None]) -> None:
|
||||
async def handle_uncertain(analysis: AnalysisResult, ctx: WorkflowContext[Never, str]) -> None:
|
||||
if analysis.spam_decision == "Uncertain":
|
||||
email: Email | None = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{analysis.email_id}")
|
||||
await ctx.add_event(
|
||||
WorkflowCompletedEvent(
|
||||
f"Email marked as uncertain: {analysis.reason}. Email content: {getattr(email, 'email_content', '')}"
|
||||
)
|
||||
await ctx.yield_output(
|
||||
f"Email marked as uncertain: {analysis.reason}. Email content: {getattr(email, 'email_content', '')}"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("This executor should only handle Uncertain messages.")
|
||||
|
||||
|
||||
@executor(id="database_access")
|
||||
async def database_access(analysis: AnalysisResult, ctx: WorkflowContext[None]) -> None:
|
||||
async def database_access(analysis: AnalysisResult, ctx: WorkflowContext[Never, str]) -> None:
|
||||
# Simulate DB writes for email and analysis (and summary if present)
|
||||
await asyncio.sleep(0.05)
|
||||
await ctx.add_event(DatabaseEvent(f"Email {analysis.email_id} saved to database."))
|
||||
@@ -263,14 +263,18 @@ async def main() -> None:
|
||||
print("Unable to find resource file, using default text.")
|
||||
email = "Hello team, here are the updates for this week..."
|
||||
|
||||
# Print outputs and database events from streaming
|
||||
async for event in workflow.run_stream(email):
|
||||
if isinstance(event, (WorkflowCompletedEvent, DatabaseEvent)):
|
||||
if isinstance(event, DatabaseEvent):
|
||||
print(f"{event}")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print(f"Workflow output: {event.data}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
WorkflowCompletedEvent(data=Email sent: Hi Alex,
|
||||
DatabaseEvent(data=Email 32021432-2d4e-4c54-b04c-f81b4120340c saved to database.)
|
||||
Workflow output: Email sent: Hi Alex,
|
||||
|
||||
Thank you for summarizing the action items from this morning's meeting.
|
||||
I have noted the three tasks and will begin working on them right away.
|
||||
@@ -281,8 +285,7 @@ async def main() -> None:
|
||||
If anything else comes up, please let me know.
|
||||
|
||||
Best regards,
|
||||
Sarah)
|
||||
DatabaseEvent(data=Email 32021432-2d4e-4c54-b04c-f81b4120340c saved to database.)
|
||||
Sarah
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
)
|
||||
|
||||
@@ -19,8 +21,8 @@ the second reverses the text and completes the workflow. The run_stream loop pri
|
||||
|
||||
Purpose:
|
||||
Show how to define explicit Executor classes with @handler methods, wire them in order with
|
||||
WorkflowBuilder, and consume streaming events. Demonstrate typed WorkflowContext[T] for outputs,
|
||||
ctx.send_message to pass intermediate values, and ctx.add_event to signal completion with a WorkflowCompletedEvent.
|
||||
WorkflowBuilder, and consume streaming events. Demonstrate typed WorkflowContext[T_Out, T_W_Out] for outputs,
|
||||
ctx.send_message to pass intermediate values, and ctx.yield_output to provide workflow outputs.
|
||||
|
||||
Prerequisites:
|
||||
- No external services required.
|
||||
@@ -44,21 +46,21 @@ class UpperCaseExecutor(Executor):
|
||||
|
||||
|
||||
class ReverseTextExecutor(Executor):
|
||||
"""Reverses the incoming string and completes the workflow.
|
||||
"""Reverses the incoming string and yields workflow output.
|
||||
|
||||
Concepts:
|
||||
- Use ctx.add_event to publish a WorkflowCompletedEvent when the terminal result is ready.
|
||||
- Use ctx.yield_output to provide workflow outputs when the terminal result is ready.
|
||||
- The terminal node does not forward messages further.
|
||||
"""
|
||||
|
||||
@handler
|
||||
async def reverse_text(self, text: str, ctx: WorkflowContext[Any]) -> None:
|
||||
"""Reverse the input string and emit a completion event."""
|
||||
async def reverse_text(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
|
||||
"""Reverse the input string and yield the workflow output."""
|
||||
result = text[::-1]
|
||||
await ctx.add_event(WorkflowCompletedEvent(result))
|
||||
await ctx.yield_output(result)
|
||||
|
||||
|
||||
async def main():
|
||||
async def main() -> None:
|
||||
"""Build a two step sequential workflow and run it with streaming to observe events."""
|
||||
# Step 1: Create executor instances.
|
||||
upper_case_executor = UpperCaseExecutor(id="upper_case_executor")
|
||||
@@ -74,15 +76,15 @@ async def main():
|
||||
)
|
||||
|
||||
# Step 3: Stream events for a single input.
|
||||
# The stream will include executor invoke and completion events, plus the final WorkflowCompletedEvent.
|
||||
completion_event = None
|
||||
# The stream will include executor invoke and completion events, plus workflow outputs.
|
||||
outputs: list[str] = []
|
||||
async for event in workflow.run_stream("hello world"):
|
||||
print(f"Event: {event}")
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion_event = event
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
outputs.append(cast(str, event.data))
|
||||
|
||||
if completion_event:
|
||||
print(f"Workflow completed with result: {completion_event.data}")
|
||||
if outputs:
|
||||
print(f"Workflow outputs: {outputs}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import WorkflowBuilder, WorkflowCompletedEvent, WorkflowContext, executor
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, executor
|
||||
|
||||
"""
|
||||
Sample: Foundational sequential workflow with streaming using function-style executors.
|
||||
|
||||
Two lightweight steps run in order. The first converts text to uppercase.
|
||||
The second reverses the text and completes the workflow. Events are printed as they arrive from run_stream.
|
||||
The second reverses the text and yields the workflow output. Events are printed as they arrive from run_stream.
|
||||
|
||||
Purpose:
|
||||
Show how to declare executors with the @executor decorator, connect them with WorkflowBuilder,
|
||||
pass intermediate values using ctx.send_message, and signal completion with ctx.add_event by emitting a
|
||||
WorkflowCompletedEvent. Demonstrate how streaming exposes ExecutorInvokedEvent and WorkflowCompletedEvent
|
||||
for observability.
|
||||
pass intermediate values using ctx.send_message, and yield final output using ctx.yield_output().
|
||||
Demonstrate how streaming exposes ExecutorInvokedEvent and ExecutorCompletedEvent for observability.
|
||||
|
||||
Prerequisites:
|
||||
- No external services required.
|
||||
@@ -37,17 +38,17 @@ async def to_upper_case(text: str, ctx: WorkflowContext[str]) -> None:
|
||||
|
||||
|
||||
@executor(id="reverse_text_executor")
|
||||
async def reverse_text(text: str, ctx: WorkflowContext[str]) -> None:
|
||||
"""Reverse the input and complete the workflow with the final result.
|
||||
async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None:
|
||||
"""Reverse the input and yield the workflow output.
|
||||
|
||||
Concepts:
|
||||
- Terminal nodes publish a WorkflowCompletedEvent using ctx.add_event.
|
||||
- No further messages are forwarded after completion.
|
||||
- Terminal nodes yield output using ctx.yield_output().
|
||||
- The workflow completes when it becomes idle (no more work to do).
|
||||
"""
|
||||
result = text[::-1]
|
||||
|
||||
# Emit the terminal event that carries the final output for this run.
|
||||
await ctx.add_event(WorkflowCompletedEvent(result))
|
||||
# Yield the final output for this workflow run.
|
||||
await ctx.yield_output(result)
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -57,17 +58,11 @@ async def main():
|
||||
workflow = WorkflowBuilder().add_edge(to_upper_case, reverse_text).set_start_executor(to_upper_case).build()
|
||||
|
||||
# Step 3: Run the workflow and stream events in real time.
|
||||
completion_event = None
|
||||
async for event in workflow.run_stream("hello world"):
|
||||
# You will see executor invoke and completion events, and then the final WorkflowCompletedEvent.
|
||||
# You will see executor invoke and completion events as the workflow progresses.
|
||||
print(f"Event: {event}")
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
# The WorkflowCompletedEvent contains the final result.
|
||||
completion_event = event
|
||||
|
||||
# Print the final result after the streaming loop concludes.
|
||||
if completion_event:
|
||||
print(f"Workflow completed with result: {completion_event.data}")
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
print(f"Workflow completed with result: {event.data}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
@@ -75,8 +70,8 @@ async def main():
|
||||
Event: ExecutorInvokedEvent(executor_id=upper_case_executor)
|
||||
Event: ExecutorCompletedEvent(executor_id=upper_case_executor)
|
||||
Event: ExecutorInvokedEvent(executor_id=reverse_text_executor)
|
||||
Event: WorkflowCompletedEvent(data=DLROW OLLEH)
|
||||
Event: ExecutorCompletedEvent(executor_id=reverse_text_executor)
|
||||
Event: WorkflowOutputEvent(data='DLROW OLLEH', source_executor_id=reverse_text_executor)
|
||||
Workflow completed with result: DLROW OLLEH
|
||||
"""
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ from agent_framework import (
|
||||
ExecutorCompletedEvent,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.azure import AzureChatClient
|
||||
@@ -25,6 +25,7 @@ Sample: Simple Loop (with an Agent Judge)
|
||||
What it does:
|
||||
- Guesser performs a binary search; judge is an agent that returns ABOVE/BELOW/MATCHED.
|
||||
- Demonstrates feedback loops in workflows with agent steps.
|
||||
- The workflow completes when the correct number is guessed.
|
||||
|
||||
Prerequisites:
|
||||
- Azure AI/ Azure OpenAI for `AzureChatClient` agent.
|
||||
@@ -55,14 +56,14 @@ class GuessNumberExecutor(Executor):
|
||||
self._upper = bound[1]
|
||||
|
||||
@handler
|
||||
async def guess_number(self, feedback: NumberSignal, ctx: WorkflowContext[int]) -> None:
|
||||
async def guess_number(self, feedback: NumberSignal, ctx: WorkflowContext[int, str]) -> None:
|
||||
"""Execute the task by guessing a number."""
|
||||
if feedback == NumberSignal.INIT:
|
||||
self._guess = (self._lower + self._upper) // 2
|
||||
await ctx.send_message(self._guess)
|
||||
elif feedback == NumberSignal.MATCHED:
|
||||
# The previous guess was correct.
|
||||
await ctx.add_event(WorkflowCompletedEvent(f"Guessed the number: {self._guess}"))
|
||||
await ctx.yield_output(f"Guessed the number: {self._guess}")
|
||||
elif feedback == NumberSignal.ABOVE:
|
||||
# The previous guess was too low.
|
||||
# Update the lower bound to the previous guess.
|
||||
@@ -150,6 +151,8 @@ async def main():
|
||||
async for event in workflow.run_stream(NumberSignal.INIT):
|
||||
if isinstance(event, ExecutorCompletedEvent) and event.executor_id == guess_number_executor.id:
|
||||
iterations += 1
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print(f"Final result: {event.data}")
|
||||
print(f"Event: {event}")
|
||||
|
||||
# This is essentially a binary search, so the number of iterations should be logarithmic.
|
||||
|
||||
@@ -6,6 +6,8 @@ from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import ( # Core chat primitives used to form LLM requests
|
||||
AgentExecutor, # Wraps an agent so it can run inside a workflow
|
||||
AgentExecutorRequest, # Message bundle sent to an AgentExecutor
|
||||
@@ -15,7 +17,6 @@ from agent_framework import ( # Core chat primitives used to form LLM requests
|
||||
Default, # Default branch when no cases match
|
||||
Role,
|
||||
WorkflowBuilder, # Fluent builder for assembling the graph
|
||||
WorkflowCompletedEvent, # Terminal event for successful completion
|
||||
WorkflowContext, # Per-run context and event bus
|
||||
executor, # Decorator to turn a function into a workflow executor
|
||||
)
|
||||
@@ -36,6 +37,7 @@ Demonstrate deterministic one of N routing with switch-case edges. Show how to:
|
||||
- Validate agent JSON with Pydantic models for robust parsing.
|
||||
- Keep executor responsibilities narrow. Transform model output to a typed DetectionResult, then route based
|
||||
on that type.
|
||||
- Use ctx.yield_output() to provide workflow results - the workflow completes when idle with no pending work.
|
||||
|
||||
Prerequisites:
|
||||
- Familiarity with WorkflowBuilder, executors, edges, and events.
|
||||
@@ -124,30 +126,28 @@ async def submit_to_email_assistant(detection: DetectionResult, ctx: WorkflowCon
|
||||
|
||||
|
||||
@executor(id="finalize_and_send")
|
||||
async def finalize_and_send(response: AgentExecutorResponse, ctx: WorkflowContext[None]) -> None:
|
||||
# Terminal step for the drafting branch. Emit a completion event with the reply.
|
||||
async def finalize_and_send(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
# Terminal step for the drafting branch. Yield the email response as output.
|
||||
parsed = EmailResponse.model_validate_json(response.agent_run_response.text)
|
||||
await ctx.add_event(WorkflowCompletedEvent(f"Email sent: {parsed.response}"))
|
||||
await ctx.yield_output(f"Email sent: {parsed.response}")
|
||||
|
||||
|
||||
@executor(id="handle_spam")
|
||||
async def handle_spam(detection: DetectionResult, ctx: WorkflowContext[None]) -> None:
|
||||
async def handle_spam(detection: DetectionResult, ctx: WorkflowContext[Never, str]) -> None:
|
||||
# Spam path terminal. Include the detector's rationale.
|
||||
if detection.spam_decision == "Spam":
|
||||
await ctx.add_event(WorkflowCompletedEvent(f"Email marked as spam: {detection.reason}"))
|
||||
await ctx.yield_output(f"Email marked as spam: {detection.reason}")
|
||||
else:
|
||||
raise RuntimeError("This executor should only handle Spam messages.")
|
||||
|
||||
|
||||
@executor(id="handle_uncertain")
|
||||
async def handle_uncertain(detection: DetectionResult, ctx: WorkflowContext[None]) -> None:
|
||||
async def handle_uncertain(detection: DetectionResult, ctx: WorkflowContext[Never, str]) -> None:
|
||||
# Uncertain path terminal. Surface the original content to aid human review.
|
||||
if detection.spam_decision == "Uncertain":
|
||||
email: Email | None = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{detection.email_id}")
|
||||
await ctx.add_event(
|
||||
WorkflowCompletedEvent(
|
||||
f"Email marked as uncertain: {detection.reason}. Email content: {getattr(email, 'email_content', '')}"
|
||||
)
|
||||
await ctx.yield_output(
|
||||
f"Email marked as uncertain: {detection.reason}. Email content: {getattr(email, 'email_content', '')}"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("This executor should only handle Uncertain messages.")
|
||||
@@ -215,10 +215,12 @@ async def main():
|
||||
"Let me know if you'd like more details."
|
||||
)
|
||||
|
||||
# Run and print the terminal event for whichever branch completes.
|
||||
async for event in workflow.run_stream(email):
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
print(f"{event}")
|
||||
# Run and print the outputs from whichever branch completes.
|
||||
events = await workflow.run(email)
|
||||
outputs = events.get_outputs()
|
||||
if outputs:
|
||||
for output in outputs:
|
||||
print(f"Workflow output: {output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user