mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Fix workflow as agent streaming output (#3649)
* WIP: with_output_from * Add with_output_from to other modules; next: workflow as agent * WIP: remove agent run events * orchestrations * WIP: update samples; next start at guessing_game_With_human_input.py * Update all samples * WIP: consolidate workflow as agent streaming vs non-streaming * Consolidate workflow as agent streaming vs non-streaming * Move request info event processing to a share method * Final pass on the samples * Fix mypy * Fix mypy * Comments --------- Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
907654a489
commit
a971d24f1e
@@ -2,22 +2,14 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentRunUpdateEvent, ChatAgent, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework import AgentResponseUpdate, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Agents in a workflow with streaming
|
||||
Sample: Azure AI Agents in a Workflow with Streaming
|
||||
|
||||
A Writer agent generates content, then a Reviewer agent critiques it.
|
||||
The workflow uses streaming so you can observe incremental AgentRunUpdateEvent chunks as each agent produces tokens.
|
||||
|
||||
Purpose:
|
||||
Show how to wire chat agents into a WorkflowBuilder pipeline by adding agents directly as edges.
|
||||
|
||||
Demonstrate:
|
||||
- Automatic streaming of agent deltas via AgentRunUpdateEvent when using run_stream().
|
||||
- Agents adapt to workflow mode: run_stream() emits incremental updates, run() emits complete responses.
|
||||
This sample shows how to create Azure AI Agents and use them in a workflow with streaming.
|
||||
|
||||
Prerequisites:
|
||||
- Azure AI Agent Service configured, along with the required environment variables.
|
||||
@@ -26,54 +18,46 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
|
||||
def create_writer_agent(client: AzureAIAgentClient) -> ChatAgent:
|
||||
return client.as_agent(
|
||||
name="Writer",
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def create_reviewer_agent(client: AzureAIAgentClient) -> ChatAgent:
|
||||
return client.as_agent(
|
||||
name="Reviewer",
|
||||
instructions=(
|
||||
"You are an excellent content reviewer. "
|
||||
"Provide actionable feedback to the writer about the provided content. "
|
||||
"Provide the feedback in the most concise manner possible."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with AzureCliCredential() as cred, AzureAIAgentClient(async_credential=cred) as client:
|
||||
# Build the workflow by adding agents directly as edges.
|
||||
# Agents adapt to workflow mode: run_stream() for incremental updates, run() for complete responses.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_agent(lambda: create_writer_agent(client), name="writer")
|
||||
.register_agent(lambda: create_reviewer_agent(client), name="reviewer", output_response=True)
|
||||
.set_start_executor("writer")
|
||||
.add_edge("writer", "reviewer")
|
||||
.build()
|
||||
async with AzureCliCredential() as cred, AzureAIAgentClient(credential=cred) as client:
|
||||
# Create two agents: a Writer and a Reviewer.
|
||||
writer_agent = client.as_agent(
|
||||
name="Writer",
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
)
|
||||
|
||||
last_executor_id: str | None = None
|
||||
reviewer_agent = client.as_agent(
|
||||
name="Reviewer",
|
||||
instructions=(
|
||||
"You are an excellent content reviewer. "
|
||||
"Provide actionable feedback to the writer about the provided content. "
|
||||
"Provide the feedback in the most concise manner possible."
|
||||
),
|
||||
)
|
||||
|
||||
# Build the workflow by adding agents directly as edges.
|
||||
# Agents adapt to workflow mode: run_stream() for incremental updates, run() for complete responses.
|
||||
workflow = WorkflowBuilder().set_start_executor(writer_agent).add_edge(writer_agent, reviewer_agent).build()
|
||||
|
||||
# Track the last author to format streaming output.
|
||||
last_author: str | None = None
|
||||
|
||||
events = workflow.run_stream("Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
async for event in events:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
eid = event.executor_id
|
||||
if eid != last_executor_id:
|
||||
if last_executor_id is not None:
|
||||
print()
|
||||
print(f"{eid}:", end=" ", flush=True)
|
||||
last_executor_id = eid
|
||||
print(event.data, end="", flush=True)
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print("\n===== Final output =====")
|
||||
print(event.data)
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
if last_author is not None:
|
||||
print() # Newline between different authors
|
||||
print(f"{author}: {update.text}", end="", flush=True)
|
||||
last_author = author
|
||||
else:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+34
-33
@@ -6,8 +6,7 @@ from typing import Final
|
||||
from agent_framework import (
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
AgentResponse,
|
||||
AgentRunUpdateEvent,
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
@@ -18,7 +17,7 @@ from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Two agents connected by a function executor bridge
|
||||
Sample: AzureOpenAI Chat Agents and an Executor in a Workflow with Streaming
|
||||
|
||||
Pipeline layout:
|
||||
research_agent -> enrich_with_references (@executor) -> final_editor_agent
|
||||
@@ -30,7 +29,6 @@ The final agent incorporates the new note and produces the polished output.
|
||||
Demonstrates:
|
||||
- Using the @executor decorator to create a function-style Workflow node.
|
||||
- Consuming an AgentExecutorResponse and forwarding an AgentExecutorRequest for the next agent.
|
||||
- Streaming AgentRunUpdateEvent events across agent + function + agent chain.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
|
||||
@@ -68,7 +66,14 @@ async def enrich_with_references(
|
||||
draft: AgentExecutorResponse,
|
||||
ctx: WorkflowContext[AgentExecutorRequest],
|
||||
) -> None:
|
||||
"""Inject a follow-up user instruction that adds an external note for the next agent."""
|
||||
"""Inject a follow-up user instruction that adds an external note for the next agent.
|
||||
|
||||
Args:
|
||||
draft: The response from the research_agent containing the initial draft. This is
|
||||
a `AgentExecutorResponse` because agents in workflows send their full response
|
||||
wrapped in this type to connected executors.
|
||||
ctx: The workflow context to send the next request.
|
||||
"""
|
||||
conversation = list(draft.full_conversation or draft.agent_response.messages)
|
||||
original_prompt = next((message.text for message in conversation if message.role == "user"), "")
|
||||
external_note = _lookup_external_note(original_prompt) or (
|
||||
@@ -82,20 +87,22 @@ async def enrich_with_references(
|
||||
)
|
||||
conversation.append(ChatMessage("user", [follow_up]))
|
||||
|
||||
# Output a new AgentExecutorRequest for the next agent in the workflow.
|
||||
# Agents in workflows handle this type and will generate a response based on the request.
|
||||
await ctx.send_message(AgentExecutorRequest(messages=conversation))
|
||||
|
||||
|
||||
def create_research_agent():
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
async def main() -> None:
|
||||
"""Run the workflow and stream combined updates from both agents."""
|
||||
# Create the agents
|
||||
research_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
name="research_agent",
|
||||
instructions=(
|
||||
"Produce a short, bullet-style briefing with two actionable ideas. Label the section as 'Initial Draft'."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def create_final_editor_agent():
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
final_editor_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
name="final_editor_agent",
|
||||
instructions=(
|
||||
"Use all conversation context (including external notes) to produce the final answer. "
|
||||
@@ -103,17 +110,11 @@ def create_final_editor_agent():
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the workflow and stream combined updates from both agents."""
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_agent(create_research_agent, name="research_agent")
|
||||
.register_agent(create_final_editor_agent, name="final_editor_agent")
|
||||
.register_executor(lambda: enrich_with_references, name="enrich_with_references")
|
||||
.set_start_executor("research_agent")
|
||||
.add_edge("research_agent", "enrich_with_references")
|
||||
.add_edge("enrich_with_references", "final_editor_agent")
|
||||
.set_start_executor(research_agent)
|
||||
.add_edge(research_agent, enrich_with_references)
|
||||
.add_edge(enrich_with_references, final_editor_agent)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -121,22 +122,22 @@ async def main() -> None:
|
||||
"Create quick workspace wellness tips for a remote analyst working across two monitors."
|
||||
)
|
||||
|
||||
last_executor: str | None = None
|
||||
# Track the last author to format streaming output.
|
||||
last_author: str | None = None
|
||||
|
||||
async for event in events:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
if event.executor_id != last_executor:
|
||||
if last_executor is not None:
|
||||
print()
|
||||
print(f"{event.executor_id}:", end=" ", flush=True)
|
||||
last_executor = event.executor_id
|
||||
print(event.data, end="", flush=True)
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print("\n\n===== Final Output =====")
|
||||
response = event.data
|
||||
if isinstance(response, AgentResponse):
|
||||
print(response.text or "(empty response)")
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
if last_author is not None:
|
||||
print("\n") # Newline between different authors
|
||||
print(f"{author}: {update.text}", end="", flush=True)
|
||||
last_author = author
|
||||
else:
|
||||
print(response if response is not None else "No response generated.")
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -2,22 +2,14 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework import AgentResponseUpdate, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Agents in a workflow with streaming
|
||||
Sample: AzureOpenAI Chat Agents in a Workflow with Streaming
|
||||
|
||||
A Writer agent generates content, then a Reviewer agent critiques it.
|
||||
The workflow uses streaming so you can observe incremental AgentRunUpdateEvent chunks as each agent produces tokens.
|
||||
|
||||
Purpose:
|
||||
Show how to wire chat agents into a WorkflowBuilder pipeline by adding agents directly as edges.
|
||||
|
||||
Demonstrate:
|
||||
- Automatic streaming of agent deltas via AgentRunUpdateEvent when using run_stream().
|
||||
- Agents adapt to workflow mode: run_stream() emits incremental updates, run() emits complete responses.
|
||||
This sample shows how to create AzureOpenAI Chat Agents and use them in a workflow with streaming.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
|
||||
@@ -26,17 +18,17 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
|
||||
def create_writer_agent():
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
async def main():
|
||||
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
|
||||
# Create the agents
|
||||
writer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
name="writer",
|
||||
)
|
||||
|
||||
|
||||
def create_reviewer_agent():
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
reviewer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
instructions=(
|
||||
"You are an excellent content reviewer."
|
||||
"Provide actionable feedback to the writer about the provided content."
|
||||
@@ -45,50 +37,28 @@ def create_reviewer_agent():
|
||||
name="reviewer",
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
|
||||
# Build the workflow using the fluent builder.
|
||||
# Set the start node and connect an edge from writer to reviewer.
|
||||
# Agents adapt to workflow mode: run_stream() for incremental updates, run() for complete responses.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_agent(create_writer_agent, name="writer")
|
||||
.register_agent(create_reviewer_agent, name="reviewer", output_response=True)
|
||||
.set_start_executor("writer")
|
||||
.add_edge("writer", "reviewer")
|
||||
.build()
|
||||
)
|
||||
workflow = WorkflowBuilder().set_start_executor(writer_agent).add_edge(writer_agent, reviewer_agent).build()
|
||||
|
||||
# Stream events from the workflow. We aggregate partial token updates per executor for readable output.
|
||||
last_executor_id: str | None = None
|
||||
# Track the last author to format streaming output.
|
||||
last_author: str | None = None
|
||||
|
||||
events = workflow.run_stream("Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
async for event in events:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
# AgentRunUpdateEvent contains incremental text deltas from the underlying agent.
|
||||
# Print a prefix when the executor changes, then append updates on the same line.
|
||||
eid = event.executor_id
|
||||
if eid != last_executor_id:
|
||||
if last_executor_id is not None:
|
||||
print()
|
||||
print(f"{eid}:", end=" ", flush=True)
|
||||
last_executor_id = eid
|
||||
print(event.data, end="", flush=True)
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print("\n===== Final output =====")
|
||||
print(event.data)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
writer_agent: Charge Up Your Journey. Fun, Affordable, Electric.
|
||||
reviewer_agent: Clear message, but consider highlighting SUV specific benefits (space, versatility) for stronger
|
||||
impact. Try more vivid language to evoke excitement. Example: "Big on Space. Big on Fun. Electric for Everyone."
|
||||
===== Final Output =====
|
||||
Clear message, but consider highlighting SUV specific benefits (space, versatility) for stronger impact. Try more
|
||||
vivid language to evoke excitement. Example: "Big on Space. Big on Fun. Electric for Everyone."
|
||||
"""
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
if last_author is not None:
|
||||
print() # Newline between different authors
|
||||
print(f"{author}: {update.text}", end="", flush=True)
|
||||
last_author = author
|
||||
else:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
-324
@@ -1,324 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
AgentResponse,
|
||||
AgentRunUpdateEvent,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
RequestInfoEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from pydantic import Field
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
Sample: Tool-enabled agents with human feedback
|
||||
|
||||
Pipeline layout:
|
||||
writer_agent (uses Azure OpenAI tools) -> Coordinator -> writer_agent
|
||||
-> Coordinator -> final_editor_agent -> Coordinator -> output
|
||||
|
||||
The writer agent calls tools to gather product facts before drafting copy. A custom executor
|
||||
packages the draft and emits a RequestInfoEvent so a human can comment, then replays the human
|
||||
guidance back into the conversation before the final editor agent produces the polished output.
|
||||
|
||||
Demonstrates:
|
||||
- Attaching Python function tools to an agent inside a workflow.
|
||||
- Capturing the writer's output for human review.
|
||||
- Streaming AgentRunUpdateEvent updates alongside human-in-the-loop pauses.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
|
||||
- Authentication via azure-identity. Run `az login` before executing.
|
||||
"""
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def fetch_product_brief(
|
||||
product_name: Annotated[str, Field(description="Product name to look up.")],
|
||||
) -> str:
|
||||
"""Return a marketing brief for a product."""
|
||||
briefs = {
|
||||
"lumenx desk lamp": (
|
||||
"Product: LumenX Desk Lamp\n"
|
||||
"- Three-point adjustable arm with 270° rotation.\n"
|
||||
"- Custom warm-to-neutral LED spectrum (2700K-4000K).\n"
|
||||
"- USB-C charging pad integrated in the base.\n"
|
||||
"- Designed for home offices and late-night study sessions."
|
||||
)
|
||||
}
|
||||
return briefs.get(product_name.lower(), f"No stored brief for '{product_name}'.")
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_brand_voice_profile(
|
||||
voice_name: Annotated[str, Field(description="Brand or campaign voice to emulate.")],
|
||||
) -> str:
|
||||
"""Return guidance for the requested brand voice."""
|
||||
voices = {
|
||||
"lumenx launch": (
|
||||
"Voice guidelines:\n"
|
||||
"- Friendly and modern with concise sentences.\n"
|
||||
"- Highlight practical benefits before aesthetics.\n"
|
||||
"- End with an invitation to imagine the product in daily use."
|
||||
)
|
||||
}
|
||||
return voices.get(voice_name.lower(), f"No stored voice profile for '{voice_name}'.")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DraftFeedbackRequest:
|
||||
"""Payload sent for human review."""
|
||||
|
||||
prompt: str = ""
|
||||
draft_text: str = ""
|
||||
conversation: list[ChatMessage] = field(default_factory=list) # type: ignore[reportUnknownVariableType]
|
||||
|
||||
|
||||
class Coordinator(Executor):
|
||||
"""Bridge between the writer agent, human feedback, and final editor."""
|
||||
|
||||
def __init__(self, id: str, writer_id: str, final_editor_id: str) -> None:
|
||||
super().__init__(id)
|
||||
self.writer_id = writer_id
|
||||
self.final_editor_id = final_editor_id
|
||||
|
||||
@handler
|
||||
async def on_writer_response(
|
||||
self,
|
||||
draft: AgentExecutorResponse,
|
||||
ctx: WorkflowContext[Never, AgentResponse],
|
||||
) -> None:
|
||||
"""Handle responses from the other two agents in the workflow."""
|
||||
if draft.executor_id == self.final_editor_id:
|
||||
# Final editor response; yield output directly.
|
||||
await ctx.yield_output(draft.agent_response)
|
||||
return
|
||||
|
||||
# Writer agent response; request human feedback.
|
||||
# Preserve the full conversation so the final editor
|
||||
# can see tool traces and the initial prompt.
|
||||
conversation: list[ChatMessage]
|
||||
if draft.full_conversation is not None:
|
||||
conversation = list(draft.full_conversation)
|
||||
else:
|
||||
conversation = list(draft.agent_response.messages)
|
||||
draft_text = draft.agent_response.text.strip()
|
||||
if not draft_text:
|
||||
draft_text = "No draft text was produced."
|
||||
|
||||
prompt = (
|
||||
"Review the draft from the writer and provide a short directional note "
|
||||
"(tone tweaks, must-have detail, target audience, etc.). "
|
||||
"Keep it under 30 words."
|
||||
)
|
||||
await ctx.request_info(
|
||||
request_data=DraftFeedbackRequest(prompt=prompt, draft_text=draft_text, conversation=conversation),
|
||||
response_type=str,
|
||||
)
|
||||
|
||||
@response_handler
|
||||
async def on_human_feedback(
|
||||
self,
|
||||
original_request: DraftFeedbackRequest,
|
||||
feedback: str,
|
||||
ctx: WorkflowContext[AgentExecutorRequest],
|
||||
) -> None:
|
||||
note = feedback.strip()
|
||||
if note.lower() == "approve":
|
||||
# Human approved the draft as-is; forward it unchanged.
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(
|
||||
messages=original_request.conversation
|
||||
+ [ChatMessage("user", text="The draft is approved as-is.")],
|
||||
should_respond=True,
|
||||
),
|
||||
target_id=self.final_editor_id,
|
||||
)
|
||||
return
|
||||
|
||||
# Human provided feedback; prompt the writer to revise.
|
||||
conversation: list[ChatMessage] = list(original_request.conversation)
|
||||
instruction = (
|
||||
"A human reviewer shared the following guidance:\n"
|
||||
f"{note or 'No specific guidance provided.'}\n\n"
|
||||
"Rewrite the draft from the previous assistant message into a polished final version. "
|
||||
"Keep the response under 120 words and reflect any requested tone adjustments."
|
||||
)
|
||||
conversation.append(ChatMessage("user", text=instruction))
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=conversation, should_respond=True), target_id=self.writer_id
|
||||
)
|
||||
|
||||
|
||||
def create_writer_agent() -> ChatAgent:
|
||||
"""Creates a writer agent with tools."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
name="writer_agent",
|
||||
instructions=(
|
||||
"You are a marketing writer. Call the available tools before drafting copy so you are precise. "
|
||||
"Always call both tools once before drafting. Summarize tool outputs as bullet points, then "
|
||||
"produce a 3-sentence draft."
|
||||
),
|
||||
tools=[fetch_product_brief, get_brand_voice_profile],
|
||||
tool_choice="required",
|
||||
)
|
||||
|
||||
|
||||
def create_final_editor_agent() -> ChatAgent:
|
||||
"""Creates a final editor agent."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
name="final_editor_agent",
|
||||
instructions=(
|
||||
"You are an editor who polishes marketing copy after human approval. "
|
||||
"Correct any legal or factual issues. Return the final version even if no changes are made. "
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def display_agent_run_update(event: AgentRunUpdateEvent, last_executor: str | None) -> None:
|
||||
"""Display an AgentRunUpdateEvent in a readable format."""
|
||||
printed_tool_calls: set[str] = set()
|
||||
printed_tool_results: set[str] = set()
|
||||
executor_id = event.executor_id
|
||||
update = event.data
|
||||
# Extract and print any new tool calls or results from the update.
|
||||
function_calls = [c for c in update.contents if isinstance(c, FunctionCallContent)] # type: ignore[union-attr]
|
||||
function_results = [c for c in update.contents if isinstance(c, FunctionResultContent)] # type: ignore[union-attr]
|
||||
if executor_id != last_executor:
|
||||
if last_executor is not None:
|
||||
print()
|
||||
print(f"{executor_id}:", end=" ", flush=True)
|
||||
last_executor = executor_id
|
||||
# Print any new tool calls before the text update.
|
||||
for call in function_calls:
|
||||
if call.call_id in printed_tool_calls:
|
||||
continue
|
||||
printed_tool_calls.add(call.call_id)
|
||||
args = call.arguments
|
||||
args_preview = json.dumps(args, ensure_ascii=False) if isinstance(args, dict) else (args or "").strip()
|
||||
print(
|
||||
f"\n{executor_id} [tool-call] {call.name}({args_preview})",
|
||||
flush=True,
|
||||
)
|
||||
print(f"{executor_id}:", end=" ", flush=True)
|
||||
# Print any new tool results before the text update.
|
||||
for result in function_results:
|
||||
if result.call_id in printed_tool_results:
|
||||
continue
|
||||
printed_tool_results.add(result.call_id)
|
||||
result_text = result.result
|
||||
if not isinstance(result_text, str):
|
||||
result_text = json.dumps(result_text, ensure_ascii=False)
|
||||
print(
|
||||
f"\n{executor_id} [tool-result] {result.call_id}: {result_text}",
|
||||
flush=True,
|
||||
)
|
||||
print(f"{executor_id}:", end=" ", flush=True)
|
||||
# Finally, print the text update.
|
||||
print(update, end="", flush=True)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the workflow and bridge human feedback between two agents."""
|
||||
|
||||
# Build the workflow.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_agent(create_writer_agent, name="writer_agent")
|
||||
.register_agent(create_final_editor_agent, name="final_editor_agent")
|
||||
.register_executor(
|
||||
lambda: Coordinator(
|
||||
id="coordinator",
|
||||
writer_id="writer_agent",
|
||||
final_editor_id="final_editor_agent",
|
||||
),
|
||||
name="coordinator",
|
||||
)
|
||||
.set_start_executor("writer_agent")
|
||||
.add_edge("writer_agent", "coordinator")
|
||||
.add_edge("coordinator", "writer_agent")
|
||||
.add_edge("final_editor_agent", "coordinator")
|
||||
.add_edge("coordinator", "final_editor_agent")
|
||||
.build()
|
||||
)
|
||||
|
||||
# Switch to turn on agent run update display.
|
||||
# By default this is off to reduce clutter during human input.
|
||||
display_agent_run_update_switch = False
|
||||
|
||||
print(
|
||||
"Interactive mode. When prompted, provide a short feedback note for the editor.",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
pending_responses: dict[str, str] | None = None
|
||||
completed = False
|
||||
initial_run = True
|
||||
|
||||
while not completed:
|
||||
last_executor: str | None = None
|
||||
if initial_run:
|
||||
stream = workflow.run_stream(
|
||||
"Create a short launch blurb for the LumenX desk lamp. Emphasize adjustability and warm lighting."
|
||||
)
|
||||
initial_run = False
|
||||
elif pending_responses is not None:
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
pending_responses = None
|
||||
else:
|
||||
break
|
||||
|
||||
requests: list[tuple[str, DraftFeedbackRequest]] = []
|
||||
|
||||
async for event in stream:
|
||||
if isinstance(event, AgentRunUpdateEvent) and display_agent_run_update_switch:
|
||||
display_agent_run_update(event, last_executor)
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, DraftFeedbackRequest):
|
||||
# Stash the request so we can prompt the human after the stream completes.
|
||||
requests.append((event.request_id, event.data))
|
||||
last_executor = None
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
last_executor = None
|
||||
response = event.data
|
||||
print("\n===== Final output =====")
|
||||
final_text = getattr(response, "text", str(response))
|
||||
print(final_text.strip())
|
||||
completed = True
|
||||
|
||||
if requests and not completed:
|
||||
responses: dict[str, str] = {}
|
||||
for request_id, request in requests:
|
||||
print("\n----- Writer draft -----")
|
||||
print(request.draft_text.strip())
|
||||
print("\nProvide guidance for the editor (or 'approve' to accept the draft).")
|
||||
answer = input("Human feedback: ").strip() # noqa: ASYNC250
|
||||
if answer.lower() == "exit":
|
||||
print("Exiting...")
|
||||
return
|
||||
responses[request_id] = answer
|
||||
pending_responses = responses
|
||||
|
||||
print("Workflow complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -20,10 +20,22 @@ Demonstrates:
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
|
||||
- Familiarity with Workflow events (AgentRunEvent, WorkflowOutputEvent)
|
||||
- Familiarity with Workflow events (WorkflowOutputEvent)
|
||||
"""
|
||||
|
||||
|
||||
def clear_and_redraw(buffers: dict[str, str], agent_order: list[str]) -> None:
|
||||
"""Clear terminal and redraw all agent outputs grouped together."""
|
||||
# ANSI escape: clear screen and move cursor to top-left
|
||||
print("\033[2J\033[H", end="")
|
||||
print("===== Concurrent Agent Streaming (Live) =====\n")
|
||||
for name in agent_order:
|
||||
print(f"--- {name} ---")
|
||||
print(buffers.get(name, ""))
|
||||
print()
|
||||
print("", end="", flush=True)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 1) Create three domain agents using AzureOpenAIChatClient
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
@@ -58,68 +70,13 @@ async def main() -> None:
|
||||
# 3) Expose the concurrent workflow as an agent for easy reuse
|
||||
agent = workflow.as_agent(name="ConcurrentWorkflowAgent")
|
||||
prompt = "We are launching a new budget-friendly electric bike for urban commuters."
|
||||
|
||||
agent_response = await agent.run(prompt)
|
||||
|
||||
if agent_response.messages:
|
||||
print("\n===== Aggregated Messages =====")
|
||||
for i, msg in enumerate(agent_response.messages, start=1):
|
||||
role = getattr(msg.role, "value", msg.role)
|
||||
name = msg.author_name if msg.author_name else role
|
||||
print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
===== Aggregated Messages =====
|
||||
------------------------------------------------------------
|
||||
|
||||
01 [user]:
|
||||
We are launching a new budget-friendly electric bike for urban commuters.
|
||||
------------------------------------------------------------
|
||||
|
||||
02 [researcher]:
|
||||
**Insights:**
|
||||
|
||||
- **Target Demographic:** Urban commuters seeking affordable, eco-friendly transport;
|
||||
likely to include students, young professionals, and price-sensitive urban residents.
|
||||
- **Market Trends:** E-bike sales are growing globally, with increasing urbanization,
|
||||
higher fuel costs, and sustainability concerns driving adoption.
|
||||
- **Competitive Landscape:** Key competitors include brands like Rad Power Bikes, Aventon,
|
||||
Lectric, and domestic budget-focused manufacturers in North America, Europe, and Asia.
|
||||
- **Feature Expectations:** Customers expect reliability, ease-of-use, theft protection,
|
||||
lightweight design, sufficient battery range for daily city commutes (typically 25-40 miles),
|
||||
and low-maintenance components.
|
||||
|
||||
**Opportunities:**
|
||||
|
||||
- **First-time Buyers:** Capture newcomers to e-biking by emphasizing affordability, ease of
|
||||
operation, and cost savings vs. public transit/car ownership.
|
||||
...
|
||||
------------------------------------------------------------
|
||||
|
||||
03 [marketer]:
|
||||
**Value Proposition:**
|
||||
"Empowering your city commute: Our new electric bike combines affordability, reliability, and
|
||||
sustainable design—helping you conquer urban journeys without breaking the bank."
|
||||
|
||||
**Target Messaging:**
|
||||
|
||||
*For Young Professionals:*
|
||||
...
|
||||
------------------------------------------------------------
|
||||
|
||||
04 [legal]:
|
||||
**Constraints, Disclaimers, & Policy Concerns for Launching a Budget-Friendly Electric Bike for Urban Commuters:**
|
||||
|
||||
**1. Regulatory Compliance**
|
||||
- Verify that the electric bike meets all applicable federal, state, and local regulations
|
||||
regarding e-bike classification, speed limits, power output, and safety features.
|
||||
- Ensure necessary certifications (e.g., UL certification for batteries, CE markings if sold internationally) are obtained.
|
||||
|
||||
**2. Product Safety**
|
||||
- Include consumer safety warnings regarding use, battery handling, charging protocols, and age restrictions.
|
||||
...
|
||||
""" # noqa: E501
|
||||
print("===== Final Aggregated Response =====\n")
|
||||
for message in agent_response.messages:
|
||||
# The agent_response contains messages from all participants concatenated
|
||||
# into a single message.
|
||||
print(f"{message.author_name}: {message.text}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -14,15 +14,17 @@ from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Step 2: Agents in a Workflow non-streaming
|
||||
Sample: Custom Agent Executors in a Workflow
|
||||
|
||||
This sample uses two custom executors. A Writer agent creates or edits content,
|
||||
then hands the conversation to a Reviewer agent which evaluates and finalizes the result.
|
||||
|
||||
Purpose:
|
||||
Show how to wrap chat agents created by AzureOpenAIChatClient inside workflow executors. Demonstrate the @handler pattern
|
||||
with typed inputs and typed WorkflowContext[T] outputs, connect executors with the fluent WorkflowBuilder, and finish
|
||||
by yielding outputs from the terminal node.
|
||||
Show how to wrap chat agents created by AzureOpenAIChatClient inside workflow executors. Demonstrate the @handler
|
||||
pattern with typed inputs and typed WorkflowContext[T] outputs, connect executors with the fluent WorkflowBuilder,
|
||||
and finish by yielding outputs from the terminal node.
|
||||
|
||||
Note: When an agent is passed to a workflow, the workflow essenatially wrap the agent in a more sophisticated executor.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
|
||||
@@ -105,17 +107,13 @@ class Reviewer(Executor):
|
||||
|
||||
async def main():
|
||||
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
|
||||
# Create the executors
|
||||
writer = Writer()
|
||||
reviewer = Reviewer()
|
||||
|
||||
# Build the workflow using the fluent builder.
|
||||
# Set the start node and connect an edge from writer to reviewer.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_executor(Writer, name="writer")
|
||||
.register_executor(Reviewer, name="reviewer")
|
||||
.set_start_executor("writer")
|
||||
.add_edge("writer", "reviewer")
|
||||
.build()
|
||||
)
|
||||
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
|
||||
|
||||
# Run the workflow with the user's initial message.
|
||||
# For foundational clarity, use run (non streaming) and print the workflow output.
|
||||
|
||||
@@ -41,6 +41,9 @@ async def main() -> None:
|
||||
)
|
||||
)
|
||||
.participants([researcher, writer])
|
||||
# Enable intermediate outputs to observe the conversation as it unfolds
|
||||
# Intermediate outputs will be emitted as WorkflowOutputEvent events
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -54,6 +57,8 @@ async def main() -> None:
|
||||
agent_result = await workflow_agent.run(task)
|
||||
|
||||
if agent_result.messages:
|
||||
# The output should contain a message from the researcher, a message from the writer,
|
||||
# and a final synthesized answer from the orchestrator.
|
||||
print("\n===== as_agent() Transcript =====")
|
||||
for i, msg in enumerate(agent_result.messages, start=1):
|
||||
role_value = getattr(msg.role, "value", msg.role)
|
||||
|
||||
@@ -7,8 +7,7 @@ from agent_framework import (
|
||||
AgentResponse,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
HandoffAgentUserRequest,
|
||||
HandoffBuilder,
|
||||
WorkflowAgent,
|
||||
@@ -37,7 +36,10 @@ Key Concepts:
|
||||
"""
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
|
||||
# See:
|
||||
# samples/getting_started/tools/function_tool_with_approval.py
|
||||
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
|
||||
"""Simulated function to process a refund for a given order number."""
|
||||
@@ -119,7 +121,7 @@ def handle_response_and_requests(response: AgentResponse) -> dict[str, HandoffAg
|
||||
if message.text:
|
||||
print(f"- {message.author_name or message.role}: {message.text}")
|
||||
for content in message.contents:
|
||||
if isinstance(content, FunctionCallContent):
|
||||
if content.type == "function_call":
|
||||
if isinstance(content.arguments, dict):
|
||||
request = WorkflowAgent.RequestInfoFunctionArgs.from_dict(content.arguments)
|
||||
elif isinstance(content.arguments, str):
|
||||
@@ -128,6 +130,7 @@ def handle_response_and_requests(response: AgentResponse) -> dict[str, HandoffAg
|
||||
raise ValueError("Invalid arguments type. Expecting a request info structure for this sample.")
|
||||
if isinstance(request.data, HandoffAgentUserRequest):
|
||||
pending_requests[request.request_id] = request.data
|
||||
|
||||
return pending_requests
|
||||
|
||||
|
||||
@@ -196,11 +199,6 @@ async def main() -> None:
|
||||
# 1. The termination condition is met, OR
|
||||
# 2. We run out of scripted responses
|
||||
while pending_requests:
|
||||
for request in pending_requests.values():
|
||||
for message in request.agent_response.messages:
|
||||
if message.text:
|
||||
print(f"- {message.author_name or message.role}: {message.text}")
|
||||
|
||||
if not scripted_responses:
|
||||
# No more scripted responses; terminate the workflow
|
||||
responses = {req_id: HandoffAgentUserRequest.terminate() for req_id in pending_requests}
|
||||
@@ -214,7 +212,7 @@ async def main() -> None:
|
||||
responses = {req_id: HandoffAgentUserRequest.create_response(user_response) for req_id in pending_requests}
|
||||
|
||||
function_results = [
|
||||
FunctionResultContent(call_id=req_id, result=response) for req_id, response in responses.items()
|
||||
Content.from_function_result(call_id=req_id, result=response) for req_id, response in responses.items()
|
||||
]
|
||||
response = await agent.run(ChatMessage("tool", function_results))
|
||||
pending_requests = handle_response_and_requests(response)
|
||||
|
||||
@@ -61,6 +61,9 @@ async def main() -> None:
|
||||
max_stall_count=3,
|
||||
max_reset_count=2,
|
||||
)
|
||||
# Enable intermediate outputs to observe the conversation as it unfolds
|
||||
# Intermediate outputs will be emitted as WorkflowOutputEvent events
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -80,9 +83,17 @@ async def main() -> None:
|
||||
# Wrap the workflow as an agent for composition scenarios
|
||||
print("\nWrapping workflow as an agent and running...")
|
||||
workflow_agent = workflow.as_agent(name="MagenticWorkflowAgent")
|
||||
async for response in workflow_agent.run_stream(task):
|
||||
|
||||
last_response_id: str | None = None
|
||||
async for update in workflow_agent.run_stream(task):
|
||||
# Fallback for any other events with text
|
||||
print(response.text, end="", flush=True)
|
||||
if last_response_id != update.response_id:
|
||||
if last_response_id is not None:
|
||||
print() # Newline between different responses
|
||||
print(f"{update.author_name}: ", end="", flush=True)
|
||||
last_response_id = update.response_id
|
||||
else:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Workflow execution failed: {e}")
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Never
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
ChatAgent,
|
||||
Executor,
|
||||
HostedCodeInterpreterTool,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
This sample demonstrates how to create a workflow that combines an AI agent executor
|
||||
with a custom executor.
|
||||
|
||||
The workflow consists of two stages:
|
||||
1. An AI agent with code interpreter capabilities that generates and executes Python code
|
||||
2. An evaluator executor that reviews the agent's output and provides a final assessment
|
||||
|
||||
Key concepts demonstrated:
|
||||
- Creating an AI agent with tool capabilities (HostedCodeInterpreterTool)
|
||||
- Building workflows using WorkflowBuilder with an agent and a custom executor
|
||||
- Using the @handler decorator in the executor to process AgentExecutorResponse from the agent
|
||||
- Connecting workflow executors with edges to create a processing pipeline
|
||||
- Yielding final outputs from terminal executors
|
||||
- Non-streaming workflow execution and result collection
|
||||
|
||||
Prerequisites:
|
||||
- Azure AI services configured with required environment variables
|
||||
- Azure CLI authentication (run 'az login' before executing)
|
||||
- Basic understanding of async Python and workflow concepts
|
||||
"""
|
||||
|
||||
|
||||
class Evaluator(Executor):
|
||||
"""Custom executor that evaluates the output from an AI agent.
|
||||
|
||||
This executor demonstrates how to:
|
||||
- Create a custom workflow executor that processes agent responses
|
||||
- Use the @handler decorator to define the processing logic
|
||||
- Access agent execution details including response text and usage metrics
|
||||
- Yield final results to complete the workflow execution
|
||||
|
||||
The evaluator checks if the agent successfully generated the Fibonacci sequence
|
||||
and provides feedback on correctness along with resource consumption details.
|
||||
"""
|
||||
|
||||
@handler
|
||||
async def handle(self, message: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
"""Evaluate the agent's response and complete the workflow with a final assessment.
|
||||
|
||||
This handler:
|
||||
1. Receives the AgentExecutorResponse containing the agent's complete interaction
|
||||
2. Checks if the expected Fibonacci sequence appears in the response text
|
||||
3. Extracts usage details (token consumption, execution time, etc.)
|
||||
4. Yields a final evaluation string to complete the workflow
|
||||
|
||||
Args:
|
||||
message: The response from the Azure AI agent containing text and metadata
|
||||
ctx: Workflow context for yielding the final output string
|
||||
"""
|
||||
target_text = "1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89"
|
||||
correctness = target_text in message.agent_response.text
|
||||
consumption = message.agent_response.usage_details
|
||||
await ctx.yield_output(f"Correctness: {correctness}, Consumption: {consumption}")
|
||||
|
||||
|
||||
def create_coding_agent(client: AzureAIAgentClient) -> ChatAgent:
|
||||
"""Create an AI agent with code interpretation capabilities.
|
||||
|
||||
This agent can generate and execute Python code to solve problems.
|
||||
|
||||
Args:
|
||||
client: The AzureAIAgentClient used to create the agent
|
||||
|
||||
Returns:
|
||||
A ChatAgent configured with coding instructions and tools
|
||||
"""
|
||||
return client.as_agent(
|
||||
name="CodingAgent",
|
||||
instructions=("You are a helpful assistant that can write and execute Python code to solve problems."),
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIAgentClient(credential=credential) as chat_client,
|
||||
):
|
||||
# Build a workflow: Agent generates code -> Evaluator assesses results
|
||||
# The agent will be wrapped in a special agent executor which produces AgentExecutorResponse
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_agent(lambda: create_coding_agent(chat_client), name="coding_agent")
|
||||
.register_executor(lambda: Evaluator(id="evaluator"), name="evaluator")
|
||||
.set_start_executor("coding_agent")
|
||||
.add_edge("coding_agent", "evaluator")
|
||||
.build()
|
||||
)
|
||||
|
||||
# Execute the workflow with a specific coding task
|
||||
results = await workflow.run(
|
||||
"Generate the fibonacci numbers to 100 using python code, show the code and execute it."
|
||||
)
|
||||
|
||||
# Extract and display the final evaluation
|
||||
outputs = results.get_outputs()
|
||||
if isinstance(outputs, list) and len(outputs) == 1:
|
||||
print("Workflow results:", outputs[0])
|
||||
else:
|
||||
raise ValueError("Unexpected workflow outputs:", outputs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -50,9 +50,7 @@ async def main() -> None:
|
||||
if agent_response.messages:
|
||||
print("\n===== Conversation =====")
|
||||
for i, msg in enumerate(agent_response.messages, start=1):
|
||||
role_value = getattr(msg.role, "value", msg.role)
|
||||
normalized_role = str(role_value).lower() if role_value is not None else "assistant"
|
||||
name = msg.author_name or ("assistant" if normalized_role == "assistant".value else "user")
|
||||
name = msg.author_name or msg.role
|
||||
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
|
||||
|
||||
"""
|
||||
|
||||
+5
-6
@@ -17,9 +17,8 @@ if str(_SAMPLES_ROOT) not in sys.path:
|
||||
|
||||
from agent_framework import ( # noqa: E402
|
||||
ChatMessage,
|
||||
Content,
|
||||
Executor,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
WorkflowAgent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
@@ -129,10 +128,10 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
# Locate the human review function call in the response messages.
|
||||
human_review_function_call: FunctionCallContent | None = None
|
||||
human_review_function_call: Content | None = None
|
||||
for message in response.messages:
|
||||
for content in message.contents:
|
||||
if isinstance(content, FunctionCallContent) and content.name == WorkflowAgent.REQUEST_INFO_FUNCTION_NAME:
|
||||
if content.name == WorkflowAgent.REQUEST_INFO_FUNCTION_NAME:
|
||||
human_review_function_call = content
|
||||
|
||||
# Handle the human review if required.
|
||||
@@ -161,8 +160,8 @@ async def main() -> None:
|
||||
human_response = ReviewResponse(request_id=request_id, feedback="Approved", approved=True)
|
||||
|
||||
# Create the function call result object to send back to the agent.
|
||||
human_review_function_result = FunctionResultContent(
|
||||
call_id=human_review_function_call.call_id,
|
||||
human_review_function_result = Content.from_function_result(
|
||||
call_id=human_review_function_call.call_id, # type: ignore
|
||||
result=human_response,
|
||||
)
|
||||
# Send the human review result back to the agent.
|
||||
|
||||
+12
-21
@@ -5,11 +5,9 @@ from dataclasses import dataclass
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
AgentRunUpdateEvent,
|
||||
AgentResponse,
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
Content,
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
@@ -31,7 +29,6 @@ approved responses are emitted to the external consumer. The workflow completes
|
||||
Key Concepts Demonstrated:
|
||||
- WorkflowAgent: Wraps a workflow to behave like a regular agent.
|
||||
- Cyclic workflow design (Worker ↔ Reviewer) for iterative improvement.
|
||||
- AgentRunUpdateEvent: Mechanism for emitting approved responses externally.
|
||||
- Structured output parsing for review feedback using Pydantic.
|
||||
- State management for pending requests and retry logic.
|
||||
|
||||
@@ -144,7 +141,9 @@ class Worker(Executor):
|
||||
self._pending_requests[request.request_id] = (request, messages)
|
||||
|
||||
@handler
|
||||
async def handle_review_response(self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest]) -> None:
|
||||
async def handle_review_response(
|
||||
self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest, AgentResponse]
|
||||
) -> None:
|
||||
print(f"Worker: Received review for request {review.request_id[:8]} - Approved: {review.approved}")
|
||||
|
||||
if review.request_id not in self._pending_requests:
|
||||
@@ -154,14 +153,8 @@ class Worker(Executor):
|
||||
|
||||
if review.approved:
|
||||
print("Worker: Response approved. Emitting to external consumer...")
|
||||
contents: list[Content] = []
|
||||
for message in request.agent_messages:
|
||||
contents.extend(message.contents)
|
||||
|
||||
# Emit approved result to external consumer via AgentRunUpdateEvent.
|
||||
await ctx.add_event(
|
||||
AgentRunUpdateEvent(self.id, data=AgentResponseUpdate(contents=contents, role="assistant"))
|
||||
)
|
||||
# Emit approved result to external consumer
|
||||
await ctx.yield_output(AgentResponse(messages=request.agent_messages))
|
||||
return
|
||||
|
||||
print(f"Worker: Response not approved. Feedback: {review.feedback}")
|
||||
@@ -169,9 +162,7 @@ class Worker(Executor):
|
||||
|
||||
# Incorporate review feedback.
|
||||
messages.append(ChatMessage("system", [review.feedback]))
|
||||
messages.append(
|
||||
ChatMessage("system", ["Please incorporate the feedback and regenerate the response."])
|
||||
)
|
||||
messages.append(ChatMessage("system", ["Please incorporate the feedback and regenerate the response."]))
|
||||
messages.extend(request.user_messages)
|
||||
|
||||
# Retry with updated prompt.
|
||||
@@ -217,13 +208,13 @@ async def main() -> None:
|
||||
print("-" * 50)
|
||||
|
||||
# Run agent in streaming mode to observe incremental updates.
|
||||
async for event in agent.run_stream(
|
||||
response = await agent.run(
|
||||
"Write code for parallel reading 1 million files on disk and write to a sorted output file."
|
||||
):
|
||||
print(f"Agent Response: {event}")
|
||||
)
|
||||
|
||||
print("=" * 50)
|
||||
print("Workflow completed!")
|
||||
print("-" * 50)
|
||||
print("Final Approved Response:")
|
||||
print(f"{response.agent_id}: {response.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user