mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Python: Rename workflow to workflows (#1007)
* Rename workflow to workflows * Update occurence of workflow to new name
This commit is contained in:
committed by
GitHub
Unverified
parent
189434dd4b
commit
b42bb700fb
@@ -0,0 +1,234 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
AgentExecutorResponse, # Output from an AgentExecutor
|
||||
ChatMessage,
|
||||
Role,
|
||||
WorkflowBuilder, # Fluent builder for wiring executors and edges
|
||||
WorkflowContext, # Per-run context and event bus
|
||||
executor, # Decorator to declare a Python function as a workflow executor
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient # Thin client wrapper for Azure OpenAI chat models
|
||||
from azure.identity import AzureCliCredential # Uses your az CLI login for credentials
|
||||
from pydantic import BaseModel # Structured outputs for safer parsing
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
Sample: Conditional routing with structured outputs
|
||||
|
||||
What this sample is:
|
||||
- A minimal decision workflow that classifies an inbound email as spam or not spam, then routes to the
|
||||
appropriate handler.
|
||||
|
||||
Purpose:
|
||||
- Show how to attach boolean edge conditions that inspect an AgentExecutorResponse.
|
||||
- Demonstrate using Pydantic models as response_format so the agent returns JSON we can validate and parse.
|
||||
- Illustrate how to transform one agent's structured result into a new AgentExecutorRequest for a downstream agent.
|
||||
|
||||
Prerequisites:
|
||||
- You understand the basics of WorkflowBuilder, executors, and events in this framework.
|
||||
- You know the concept of edge conditions and how they gate routes using a predicate function.
|
||||
- Azure OpenAI access is configured for AzureOpenAIChatClient. You should be logged in with Azure CLI (AzureCliCredential)
|
||||
and have the Azure OpenAI environment variables set as documented in the getting started chat client README.
|
||||
- The sample email resource file exists at workflow/resources/email.txt.
|
||||
|
||||
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
|
||||
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 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.
|
||||
"""
|
||||
|
||||
|
||||
class DetectionResult(BaseModel):
|
||||
"""Represents the result of spam detection."""
|
||||
|
||||
# is_spam drives the routing decision taken by edge conditions
|
||||
is_spam: bool
|
||||
# Human readable rationale from the detector
|
||||
reason: str
|
||||
# The agent must include the original email so downstream agents can operate without reloading content
|
||||
email_content: str
|
||||
|
||||
|
||||
class EmailResponse(BaseModel):
|
||||
"""Represents the response from the email assistant."""
|
||||
|
||||
# The drafted reply that a user could copy or send
|
||||
response: str
|
||||
|
||||
|
||||
def get_condition(expected_result: bool):
|
||||
"""Create a condition callable that routes based on DetectionResult.is_spam."""
|
||||
|
||||
# The returned function will be used as an edge predicate.
|
||||
# It receives whatever the upstream executor produced.
|
||||
def condition(message: Any) -> bool:
|
||||
# Defensive guard. If a non AgentExecutorResponse appears, let the edge pass to avoid dead ends.
|
||||
if not isinstance(message, AgentExecutorResponse):
|
||||
return True
|
||||
|
||||
try:
|
||||
# Prefer parsing a structured DetectionResult from the agent JSON text.
|
||||
# Using model_validate_json ensures type safety and raises if the shape is wrong.
|
||||
detection = DetectionResult.model_validate_json(message.agent_run_response.text)
|
||||
# Route only when the spam flag matches the expected path.
|
||||
return detection.is_spam == expected_result
|
||||
except Exception:
|
||||
# Fail closed on parse errors so we do not accidentally route to the wrong path.
|
||||
# Returning False prevents this edge from activating.
|
||||
return False
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
@executor(id="send_email")
|
||||
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.yield_output(f"Email sent:\n{email_response.response}")
|
||||
|
||||
|
||||
@executor(id="handle_spam")
|
||||
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.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.")
|
||||
|
||||
|
||||
@executor(id="to_email_assistant_request")
|
||||
async def to_email_assistant_request(
|
||||
response: AgentExecutorResponse, ctx: WorkflowContext[AgentExecutorRequest]
|
||||
) -> None:
|
||||
"""Transform detection result into an AgentExecutorRequest for the email assistant.
|
||||
|
||||
Extracts DetectionResult.email_content and forwards it as a user message.
|
||||
"""
|
||||
# Bridge executor. Converts a structured DetectionResult into a ChatMessage and forwards it as a new request.
|
||||
detection = DetectionResult.model_validate_json(response.agent_run_response.text)
|
||||
user_msg = ChatMessage(Role.USER, text=detection.email_content)
|
||||
await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Create agents
|
||||
# AzureCliCredential uses your current az login. This avoids embedding secrets in code.
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Agent 1. Classifies spam and returns a DetectionResult object.
|
||||
# response_format enforces that the LLM returns parsable JSON for the Pydantic model.
|
||||
spam_detection_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a spam detection assistant that identifies spam emails. "
|
||||
"Always return JSON with fields is_spam (bool), reason (string), and email_content (string). "
|
||||
"Include the original email content in email_content."
|
||||
),
|
||||
response_format=DetectionResult,
|
||||
),
|
||||
id="spam_detection_agent",
|
||||
)
|
||||
|
||||
# Agent 2. Drafts a professional reply. Also uses structured JSON output for reliability.
|
||||
email_assistant_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are an email assistant that helps users draft professional responses to emails. "
|
||||
"Your input may be a JSON object that includes 'email_content'; base your reply on that content. "
|
||||
"Return JSON with a single field 'response' containing the drafted reply."
|
||||
),
|
||||
response_format=EmailResponse,
|
||||
),
|
||||
id="email_assistant_agent",
|
||||
)
|
||||
|
||||
# Build the workflow graph.
|
||||
# Start at the spam detector.
|
||||
# If not spam, hop to a transformer that creates a new AgentExecutorRequest,
|
||||
# then call the email assistant, then finalize.
|
||||
# If spam, go directly to the spam handler and finalize.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(spam_detection_agent)
|
||||
# Not spam path: transform response -> request for assistant -> assistant -> send email
|
||||
.add_edge(spam_detection_agent, to_email_assistant_request, condition=get_condition(False))
|
||||
.add_edge(to_email_assistant_request, email_assistant_agent)
|
||||
.add_edge(email_assistant_agent, handle_email_response)
|
||||
# Spam path: send to spam handler
|
||||
.add_edge(spam_detection_agent, handle_spam_classifier_response, condition=get_condition(True))
|
||||
.build()
|
||||
)
|
||||
|
||||
# Read Email content from the sample resource file.
|
||||
# This keeps the sample deterministic since the model sees the same email every run.
|
||||
email_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "email.txt")
|
||||
|
||||
with open(email_path) as email_file: # noqa: ASYNC230
|
||||
email = email_file.read()
|
||||
|
||||
# Execute the workflow. Since the start is an AgentExecutor, pass an AgentExecutorRequest.
|
||||
# The workflow completes when it becomes idle (no more work to do).
|
||||
request = AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email)], should_respond=True)
|
||||
events = await workflow.run(request)
|
||||
outputs = events.get_outputs()
|
||||
if outputs:
|
||||
print(f"Workflow output: {outputs[0]}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
Processing email:
|
||||
Subject: Team Meeting Follow-up - Action Items
|
||||
|
||||
Hi Sarah,
|
||||
|
||||
I wanted to follow up on our team meeting this morning and share the action items we discussed:
|
||||
|
||||
1. Update the project timeline by Friday
|
||||
2. Schedule client presentation for next week
|
||||
3. Review the budget allocation for Q4
|
||||
|
||||
Please let me know if you have any questions or if I missed anything from our discussion.
|
||||
|
||||
Best regards,
|
||||
Alex Johnson
|
||||
Project Manager
|
||||
Tech Solutions Inc.
|
||||
alex.johnson@techsolutions.com
|
||||
(555) 123-4567
|
||||
----------------------------------------
|
||||
|
||||
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.
|
||||
|
||||
I will update the project timeline by Friday, begin scheduling the client presentation for next week, and start reviewing the Q4 budget allocation. If any questions or issues arise, I'll reach out.
|
||||
|
||||
Thank you again for outlining the next steps.
|
||||
|
||||
Best regards,
|
||||
Sarah
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,292 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Step 06b — Multi-Selection Edge Group sample."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutor,
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
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. 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.
|
||||
Show how to:
|
||||
- Implement a selection function that chooses one or more downstream branches based on analysis.
|
||||
- Share state across branches so different executors can read the same email content.
|
||||
- Validate agent outputs with Pydantic models for robust structured data exchange.
|
||||
- Merge results from multiple branches (e.g., a summary) back into a typed state.
|
||||
- Apply conditional persistence logic (short vs long emails).
|
||||
|
||||
Prerequisites:
|
||||
- Familiarity with WorkflowBuilder, executors, edges, and events.
|
||||
- Understanding of multi-selection edge groups and how their selection function maps to target ids.
|
||||
- Experience with shared state in workflows for persisting and reusing objects.
|
||||
"""
|
||||
|
||||
|
||||
EMAIL_STATE_PREFIX = "email:"
|
||||
CURRENT_EMAIL_ID_KEY = "current_email_id"
|
||||
LONG_EMAIL_THRESHOLD = 100
|
||||
|
||||
|
||||
class AnalysisResultAgent(BaseModel):
|
||||
spam_decision: Literal["NotSpam", "Spam", "Uncertain"]
|
||||
reason: str
|
||||
|
||||
|
||||
class EmailResponse(BaseModel):
|
||||
response: str
|
||||
|
||||
|
||||
class EmailSummaryModel(BaseModel):
|
||||
summary: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Email:
|
||||
email_id: str
|
||||
email_content: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisResult:
|
||||
spam_decision: str
|
||||
reason: str
|
||||
email_length: int
|
||||
email_summary: str
|
||||
email_id: str
|
||||
|
||||
|
||||
class DatabaseEvent(WorkflowEvent): ...
|
||||
|
||||
|
||||
@executor(id="store_email")
|
||||
async def store_email(email_text: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
|
||||
new_email = Email(email_id=str(uuid4()), email_content=email_text)
|
||||
await ctx.set_shared_state(f"{EMAIL_STATE_PREFIX}{new_email.email_id}", new_email)
|
||||
await ctx.set_shared_state(CURRENT_EMAIL_ID_KEY, new_email.email_id)
|
||||
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=new_email.email_content)], should_respond=True)
|
||||
)
|
||||
|
||||
|
||||
@executor(id="to_analysis_result")
|
||||
async def to_analysis_result(response: AgentExecutorResponse, ctx: WorkflowContext[AnalysisResult]) -> None:
|
||||
parsed = AnalysisResultAgent.model_validate_json(response.agent_run_response.text)
|
||||
email_id: str = await ctx.get_shared_state(CURRENT_EMAIL_ID_KEY)
|
||||
email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{email_id}")
|
||||
await ctx.send_message(
|
||||
AnalysisResult(
|
||||
spam_decision=parsed.spam_decision,
|
||||
reason=parsed.reason,
|
||||
email_length=len(email.email_content),
|
||||
email_summary="",
|
||||
email_id=email_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@executor(id="submit_to_email_assistant")
|
||||
async def submit_to_email_assistant(analysis: AnalysisResult, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
|
||||
if analysis.spam_decision != "NotSpam":
|
||||
raise RuntimeError("This executor should only handle NotSpam messages.")
|
||||
|
||||
email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{analysis.email_id}")
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email.email_content)], should_respond=True)
|
||||
)
|
||||
|
||||
|
||||
@executor(id="finalize_and_send")
|
||||
async def finalize_and_send(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
parsed = EmailResponse.model_validate_json(response.agent_run_response.text)
|
||||
await ctx.yield_output(f"Email sent: {parsed.response}")
|
||||
|
||||
|
||||
@executor(id="summarize_email")
|
||||
async def summarize_email(analysis: AnalysisResult, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
|
||||
# Only called for long NotSpam emails by selection_func
|
||||
email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{analysis.email_id}")
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email.email_content)], should_respond=True)
|
||||
)
|
||||
|
||||
|
||||
@executor(id="merge_summary")
|
||||
async def merge_summary(response: AgentExecutorResponse, ctx: WorkflowContext[AnalysisResult]) -> None:
|
||||
summary = EmailSummaryModel.model_validate_json(response.agent_run_response.text)
|
||||
email_id: str = await ctx.get_shared_state(CURRENT_EMAIL_ID_KEY)
|
||||
email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{email_id}")
|
||||
# Build an AnalysisResult mirroring to_analysis_result but with summary
|
||||
await ctx.send_message(
|
||||
AnalysisResult(
|
||||
spam_decision="NotSpam",
|
||||
reason="",
|
||||
email_length=len(email.email_content),
|
||||
email_summary=summary.summary,
|
||||
email_id=email_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@executor(id="handle_spam")
|
||||
async def handle_spam(analysis: AnalysisResult, ctx: WorkflowContext[Never, str]) -> None:
|
||||
if analysis.spam_decision == "Spam":
|
||||
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[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.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[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."))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Agents
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
email_analysis_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a spam detection assistant that identifies spam emails. "
|
||||
"Always return JSON with fields 'spam_decision' (one of NotSpam, Spam, Uncertain) "
|
||||
"and 'reason' (string)."
|
||||
),
|
||||
response_format=AnalysisResultAgent,
|
||||
),
|
||||
id="email_analysis_agent",
|
||||
)
|
||||
|
||||
email_assistant_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are an email assistant that helps users draft responses to emails with professionalism."
|
||||
),
|
||||
response_format=EmailResponse,
|
||||
),
|
||||
id="email_assistant_agent",
|
||||
)
|
||||
|
||||
email_summary_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=("You are an assistant that helps users summarize emails."),
|
||||
response_format=EmailSummaryModel,
|
||||
),
|
||||
id="email_summary_agent",
|
||||
)
|
||||
|
||||
# Build the workflow
|
||||
def select_targets(analysis: AnalysisResult, target_ids: list[str]) -> list[str]:
|
||||
# Order: [handle_spam, submit_to_email_assistant, summarize_email, handle_uncertain]
|
||||
handle_spam_id, submit_to_email_assistant_id, summarize_email_id, handle_uncertain_id = target_ids
|
||||
if analysis.spam_decision == "Spam":
|
||||
return [handle_spam_id]
|
||||
if analysis.spam_decision == "NotSpam":
|
||||
targets = [submit_to_email_assistant_id]
|
||||
if analysis.email_length > LONG_EMAIL_THRESHOLD:
|
||||
targets.append(summarize_email_id)
|
||||
return targets
|
||||
return [handle_uncertain_id]
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(store_email)
|
||||
.add_edge(store_email, email_analysis_agent)
|
||||
.add_edge(email_analysis_agent, to_analysis_result)
|
||||
.add_multi_selection_edge_group(
|
||||
to_analysis_result,
|
||||
[handle_spam, submit_to_email_assistant, summarize_email, handle_uncertain],
|
||||
selection_func=select_targets,
|
||||
)
|
||||
.add_edge(submit_to_email_assistant, email_assistant_agent)
|
||||
.add_edge(email_assistant_agent, finalize_and_send)
|
||||
.add_edge(summarize_email, email_summary_agent)
|
||||
.add_edge(email_summary_agent, merge_summary)
|
||||
# Save to DB if short (no summary path)
|
||||
.add_edge(to_analysis_result, database_access, condition=lambda r: r.email_length <= LONG_EMAIL_THRESHOLD)
|
||||
# Save to DB with summary when long
|
||||
.add_edge(merge_summary, database_access)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Read an email sample
|
||||
resources_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
|
||||
"resources",
|
||||
"email.txt",
|
||||
)
|
||||
if os.path.exists(resources_path):
|
||||
with open(resources_path, encoding="utf-8") as f: # noqa: ASYNC230
|
||||
email = f.read()
|
||||
else:
|
||||
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, DatabaseEvent):
|
||||
print(f"{event}")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print(f"Workflow output: {event.data}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
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.
|
||||
I'll aim to have the updated project timeline ready by Friday and will
|
||||
coordinate with the team to schedule the client presentation for next week.
|
||||
I'll also review the Q4 budget allocation and share my feedback soon.
|
||||
|
||||
If anything else comes up, please let me know.
|
||||
|
||||
Best regards,
|
||||
Sarah
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
)
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
Sample: Sequential workflow with streaming.
|
||||
|
||||
Two custom executors run in sequence. The first converts text to uppercase,
|
||||
the second reverses the text and completes the workflow. The run_stream loop prints events as they occur.
|
||||
|
||||
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_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.
|
||||
"""
|
||||
|
||||
|
||||
class UpperCaseExecutor(Executor):
|
||||
"""Converts an input string to uppercase and forwards it.
|
||||
|
||||
Concepts:
|
||||
- @handler methods define invokable steps.
|
||||
- WorkflowContext[str] indicates this step emits a string to the next node.
|
||||
"""
|
||||
|
||||
@handler
|
||||
async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None:
|
||||
"""Transform the input to uppercase and send it downstream."""
|
||||
result = text.upper()
|
||||
# Pass the intermediate result to the next executor in the chain.
|
||||
await ctx.send_message(result)
|
||||
|
||||
|
||||
class ReverseTextExecutor(Executor):
|
||||
"""Reverses the incoming string and yields workflow output.
|
||||
|
||||
Concepts:
|
||||
- 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[Never, str]) -> None:
|
||||
"""Reverse the input string and yield the workflow output."""
|
||||
result = text[::-1]
|
||||
await ctx.yield_output(result)
|
||||
|
||||
|
||||
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")
|
||||
reverse_text_executor = ReverseTextExecutor(id="reverse_text_executor")
|
||||
|
||||
# Step 2: Build the workflow graph.
|
||||
# Order matters. We connect upper_case_executor -> reverse_text_executor and set the start.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.add_edge(upper_case_executor, reverse_text_executor)
|
||||
.set_start_executor(upper_case_executor)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Step 3: Stream events for a single input.
|
||||
# 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, WorkflowOutputEvent):
|
||||
outputs.append(cast(str, event.data))
|
||||
|
||||
if outputs:
|
||||
print(f"Workflow outputs: {outputs}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,79 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, executor
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
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 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 yield final output using ctx.yield_output().
|
||||
Demonstrate how streaming exposes ExecutorInvokedEvent and ExecutorCompletedEvent for observability.
|
||||
|
||||
Prerequisites:
|
||||
- No external services required.
|
||||
"""
|
||||
|
||||
|
||||
# Step 1: Define methods using the executor decorator.
|
||||
@executor(id="upper_case_executor")
|
||||
async def to_upper_case(text: str, ctx: WorkflowContext[str]) -> None:
|
||||
"""Transform the input to uppercase and forward it to the next step.
|
||||
|
||||
Concepts:
|
||||
- The @executor decorator registers this function as a workflow node.
|
||||
- WorkflowContext[str] indicates that this node emits a string payload downstream.
|
||||
"""
|
||||
result = text.upper()
|
||||
|
||||
# Send the intermediate result to the next executor in the workflow graph.
|
||||
await ctx.send_message(result)
|
||||
|
||||
|
||||
@executor(id="reverse_text_executor")
|
||||
async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None:
|
||||
"""Reverse the input and yield the workflow output.
|
||||
|
||||
Concepts:
|
||||
- Terminal nodes yield output using ctx.yield_output().
|
||||
- The workflow completes when it becomes idle (no more work to do).
|
||||
"""
|
||||
result = text[::-1]
|
||||
|
||||
# Yield the final output for this workflow run.
|
||||
await ctx.yield_output(result)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Build a two-step sequential workflow and run it with streaming to observe events."""
|
||||
# Step 2: Build the workflow with the defined edges.
|
||||
# Order matters. upper_case_executor runs first, then reverse_text_executor.
|
||||
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.
|
||||
async for event in workflow.run_stream("hello world"):
|
||||
# You will see executor invoke and completion events as the workflow progresses.
|
||||
print(f"Event: {event}")
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
print(f"Workflow completed with result: {event.data}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
Event: ExecutorInvokedEvent(executor_id=upper_case_executor)
|
||||
Event: ExecutorCompletedEvent(executor_id=upper_case_executor)
|
||||
Event: ExecutorInvokedEvent(executor_id=reverse_text_executor)
|
||||
Event: ExecutorCompletedEvent(executor_id=reverse_text_executor)
|
||||
Event: WorkflowOutputEvent(data='DLROW OLLEH', source_executor_id=reverse_text_executor)
|
||||
Workflow completed with result: DLROW OLLEH
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,165 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutor,
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
ExecutorCompletedEvent,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
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 `AzureOpenAIChatClient` agent.
|
||||
- Authentication via `azure-identity` — uses `AzureCliCredential()` (run `az login`).
|
||||
"""
|
||||
|
||||
|
||||
class NumberSignal(Enum):
|
||||
"""Enum to represent number signals for the workflow."""
|
||||
|
||||
# The target number is above the guess.
|
||||
ABOVE = "above"
|
||||
# The target number is below the guess.
|
||||
BELOW = "below"
|
||||
# The guess matches the target number.
|
||||
MATCHED = "matched"
|
||||
# Initial signal to start the guessing process.
|
||||
INIT = "init"
|
||||
|
||||
|
||||
class GuessNumberExecutor(Executor):
|
||||
"""An executor that guesses a number."""
|
||||
|
||||
def __init__(self, bound: tuple[int, int], id: str | None = None):
|
||||
"""Initialize the executor with a target number."""
|
||||
super().__init__(id=id or "guess_number")
|
||||
self._lower = bound[0]
|
||||
self._upper = bound[1]
|
||||
|
||||
@handler
|
||||
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.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.
|
||||
# Generate a new number that is between the new bounds.
|
||||
self._lower = self._guess + 1
|
||||
self._guess = (self._lower + self._upper) // 2
|
||||
await ctx.send_message(self._guess)
|
||||
else:
|
||||
# The previous guess was too high.
|
||||
# Update the upper bound to the previous guess.
|
||||
# Generate a new number that is between the new bounds.
|
||||
self._upper = self._guess - 1
|
||||
self._guess = (self._lower + self._upper) // 2
|
||||
await ctx.send_message(self._guess)
|
||||
|
||||
|
||||
class SubmitToJudgeAgent(Executor):
|
||||
"""Send the numeric guess to a judge agent which replies ABOVE/BELOW/MATCHED."""
|
||||
|
||||
def __init__(self, judge_agent_id: str, target: int, id: str | None = None):
|
||||
super().__init__(id=id or "submit_to_judge")
|
||||
self._judge_agent_id = judge_agent_id
|
||||
self._target = target
|
||||
|
||||
@handler
|
||||
async def submit(self, guess: int, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
|
||||
prompt = (
|
||||
"You are a number judge. Given a target number and a guess, reply with exactly one token:"
|
||||
" 'MATCHED' if guess == target, 'ABOVE' if the target is above the guess,"
|
||||
" or 'BELOW' if the target is below.\n"
|
||||
f"Target: {self._target}\nGuess: {guess}\nResponse:"
|
||||
)
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=prompt)], should_respond=True),
|
||||
target_id=self._judge_agent_id,
|
||||
)
|
||||
|
||||
|
||||
class ParseJudgeResponse(Executor):
|
||||
"""Parse AgentExecutorResponse into NumberSignal for the loop."""
|
||||
|
||||
@handler
|
||||
async def parse(self, response: AgentExecutorResponse, ctx: WorkflowContext[NumberSignal]) -> None:
|
||||
text = response.agent_run_response.text.strip().upper()
|
||||
if "MATCHED" in text:
|
||||
await ctx.send_message(NumberSignal.MATCHED)
|
||||
elif "ABOVE" in text and "BELOW" not in text:
|
||||
await ctx.send_message(NumberSignal.ABOVE)
|
||||
else:
|
||||
await ctx.send_message(NumberSignal.BELOW)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to run the workflow."""
|
||||
# Step 1: Create the executors.
|
||||
guess_number_executor = GuessNumberExecutor((1, 100))
|
||||
|
||||
# Agent judge setup
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
judge_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You strictly respond with one of: MATCHED, ABOVE, BELOW based on the given target and guess."
|
||||
)
|
||||
),
|
||||
id="judge_agent",
|
||||
)
|
||||
submit_to_judge = SubmitToJudgeAgent(judge_agent_id=judge_agent.id, target=30, id="submit_judge")
|
||||
parse_judge = ParseJudgeResponse(id="parse_judge")
|
||||
|
||||
# Step 2: Build the workflow with the defined edges.
|
||||
# This time we are creating a loop in the workflow.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.add_edge(guess_number_executor, submit_to_judge)
|
||||
.add_edge(submit_to_judge, judge_agent)
|
||||
.add_edge(judge_agent, parse_judge)
|
||||
.add_edge(parse_judge, guess_number_executor)
|
||||
.set_start_executor(guess_number_executor)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Step 3: Run the workflow and print the events.
|
||||
iterations = 0
|
||||
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.
|
||||
# The maximum number of iterations is [log2(range size)]. For a range of 1 to 100, this is log2(100) which is 7.
|
||||
# Subtract because the last round is the MATCHED event.
|
||||
print(f"Guessed {iterations - 1} times.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,226 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
from uuid import uuid4
|
||||
|
||||
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
|
||||
AgentExecutorResponse, # Result returned by an AgentExecutor
|
||||
Case, # Case entry for a switch-case edge group
|
||||
ChatMessage,
|
||||
Default, # Default branch when no cases match
|
||||
Role,
|
||||
WorkflowBuilder, # Fluent builder for assembling the graph
|
||||
WorkflowContext, # Per-run context and event bus
|
||||
executor, # Decorator to turn a function into a workflow executor
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient # Thin client for Azure OpenAI chat models
|
||||
from azure.identity import AzureCliCredential # Uses your az CLI login for credentials
|
||||
from pydantic import BaseModel # Structured outputs with validation
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
Sample: Switch-Case Edge Group with an explicit Uncertain branch.
|
||||
|
||||
The workflow stores a single email in shared state, asks a spam detection agent for a three way decision,
|
||||
then routes with a switch-case group: NotSpam to the drafting assistant, Spam to a spam handler, and
|
||||
Default to an Uncertain handler.
|
||||
|
||||
Purpose:
|
||||
Demonstrate deterministic one of N routing with switch-case edges. Show how to:
|
||||
- Persist input once in shared state, then pass around a small typed pointer that carries the email id.
|
||||
- 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.
|
||||
- Understanding of switch-case edge groups and how Case and Default are evaluated in order.
|
||||
- Working Azure OpenAI configuration for AzureOpenAIChatClient, with Azure CLI login and required environment variables.
|
||||
- Access to workflow/resources/ambiguous_email.txt, or accept the inline fallback string.
|
||||
"""
|
||||
|
||||
|
||||
EMAIL_STATE_PREFIX = "email:"
|
||||
CURRENT_EMAIL_ID_KEY = "current_email_id"
|
||||
|
||||
|
||||
class DetectionResultAgent(BaseModel):
|
||||
"""Structured output returned by the spam detection agent."""
|
||||
|
||||
# The agent classifies the email and provides a rationale.
|
||||
spam_decision: Literal["NotSpam", "Spam", "Uncertain"]
|
||||
reason: str
|
||||
|
||||
|
||||
class EmailResponse(BaseModel):
|
||||
"""Structured output returned by the email assistant agent."""
|
||||
|
||||
# The drafted professional reply.
|
||||
response: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class DetectionResult:
|
||||
# Internal typed payload used for routing and downstream handling.
|
||||
spam_decision: str
|
||||
reason: str
|
||||
email_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Email:
|
||||
# In memory record of the email content stored in shared state.
|
||||
email_id: str
|
||||
email_content: str
|
||||
|
||||
|
||||
def get_case(expected_decision: str):
|
||||
"""Factory that returns a predicate matching a specific spam_decision value."""
|
||||
|
||||
def condition(message: Any) -> bool:
|
||||
# Only match when the upstream payload is a DetectionResult with the expected decision.
|
||||
return isinstance(message, DetectionResult) and message.spam_decision == expected_decision
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
@executor(id="store_email")
|
||||
async def store_email(email_text: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
|
||||
# Persist the raw email once. Store under a unique key and set the current pointer for convenience.
|
||||
new_email = Email(email_id=str(uuid4()), email_content=email_text)
|
||||
await ctx.set_shared_state(f"{EMAIL_STATE_PREFIX}{new_email.email_id}", new_email)
|
||||
await ctx.set_shared_state(CURRENT_EMAIL_ID_KEY, new_email.email_id)
|
||||
|
||||
# Kick off the detector by forwarding the email as a user message to the spam_detection_agent.
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=new_email.email_content)], should_respond=True)
|
||||
)
|
||||
|
||||
|
||||
@executor(id="to_detection_result")
|
||||
async def to_detection_result(response: AgentExecutorResponse, ctx: WorkflowContext[DetectionResult]) -> None:
|
||||
# Parse the detector JSON into a typed model. Attach the current email id for downstream lookups.
|
||||
parsed = DetectionResultAgent.model_validate_json(response.agent_run_response.text)
|
||||
email_id: str = await ctx.get_shared_state(CURRENT_EMAIL_ID_KEY)
|
||||
await ctx.send_message(DetectionResult(spam_decision=parsed.spam_decision, reason=parsed.reason, email_id=email_id))
|
||||
|
||||
|
||||
@executor(id="submit_to_email_assistant")
|
||||
async def submit_to_email_assistant(detection: DetectionResult, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
|
||||
# Only proceed for the NotSpam branch. Guard against accidental misrouting.
|
||||
if detection.spam_decision != "NotSpam":
|
||||
raise RuntimeError("This executor should only handle NotSpam messages.")
|
||||
|
||||
# Load the original content from shared state using the id carried in DetectionResult.
|
||||
email: Email = await ctx.get_shared_state(f"{EMAIL_STATE_PREFIX}{detection.email_id}")
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=email.email_content)], should_respond=True)
|
||||
)
|
||||
|
||||
|
||||
@executor(id="finalize_and_send")
|
||||
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.yield_output(f"Email sent: {parsed.response}")
|
||||
|
||||
|
||||
@executor(id="handle_spam")
|
||||
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.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[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.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.")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to run the workflow."""
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Agents. response_format enforces that the LLM returns JSON that Pydantic can validate.
|
||||
spam_detection_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a spam detection assistant that identifies spam emails. "
|
||||
"Be less confident in your assessments. "
|
||||
"Always return JSON with fields 'spam_decision' (one of NotSpam, Spam, Uncertain) "
|
||||
"and 'reason' (string)."
|
||||
),
|
||||
response_format=DetectionResultAgent,
|
||||
),
|
||||
id="spam_detection_agent",
|
||||
)
|
||||
|
||||
email_assistant_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are an email assistant that helps users draft responses to emails with professionalism."
|
||||
),
|
||||
response_format=EmailResponse,
|
||||
),
|
||||
id="email_assistant_agent",
|
||||
)
|
||||
|
||||
# Build workflow: store -> detection agent -> to_detection_result -> switch (NotSpam or Spam or Default).
|
||||
# The switch-case group evaluates cases in order, then falls back to Default when none match.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(store_email)
|
||||
.add_edge(store_email, spam_detection_agent)
|
||||
.add_edge(spam_detection_agent, to_detection_result)
|
||||
.add_switch_case_edge_group(
|
||||
to_detection_result,
|
||||
[
|
||||
Case(condition=get_case("NotSpam"), target=submit_to_email_assistant),
|
||||
Case(condition=get_case("Spam"), target=handle_spam),
|
||||
Default(target=handle_uncertain),
|
||||
],
|
||||
)
|
||||
.add_edge(submit_to_email_assistant, email_assistant_agent)
|
||||
.add_edge(email_assistant_agent, finalize_and_send)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Read ambiguous email if available. Otherwise use a simple inline sample.
|
||||
resources_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "ambiguous_email.txt"
|
||||
)
|
||||
if os.path.exists(resources_path):
|
||||
with open(resources_path, encoding="utf-8") as f: # noqa: ASYNC230
|
||||
email = f.read()
|
||||
else:
|
||||
print("Unable to find resource file, using default text.")
|
||||
email = (
|
||||
"Hey there, I noticed you might be interested in our latest offer—no pressure, but it expires soon. "
|
||||
"Let me know if you'd like more details."
|
||||
)
|
||||
|
||||
# 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__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user