test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentRunUpdateEvent, ChatAgent, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: 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.
|
||||
|
||||
Prerequisites:
|
||||
- Azure AI Agent Service configured, along with the required environment variables.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
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()
|
||||
)
|
||||
|
||||
last_executor_id: 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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Final
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
AgentResponse,
|
||||
AgentRunUpdateEvent,
|
||||
ChatMessage,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Two agents connected by a function executor bridge
|
||||
|
||||
Pipeline layout:
|
||||
research_agent -> enrich_with_references (@executor) -> final_editor_agent
|
||||
|
||||
The first agent drafts a short answer. A lightweight @executor function simulates
|
||||
an external data fetch and injects a follow-up user message containing extra context.
|
||||
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.
|
||||
- Authentication via azure-identity. Run `az login` before executing.
|
||||
"""
|
||||
|
||||
# Simulated external content keyed by a simple topic hint.
|
||||
EXTERNAL_REFERENCES: Final[dict[str, str]] = {
|
||||
"workspace": (
|
||||
"From Workspace Weekly: Adjustable monitor arms and sit-stand desks can reduce "
|
||||
"neck strain by up to 30%. Consider adding a reminder to move every 45 minutes."
|
||||
),
|
||||
"travel": (
|
||||
"Checklist excerpt: Always confirm baggage limits for budget airlines. "
|
||||
"Keep a photocopy of your passport stored separately from the original."
|
||||
),
|
||||
"wellness": (
|
||||
"Recent survey: Employees who take two 5-minute breaks per hour report 18% higher focus "
|
||||
"scores. Encourage scheduling micro-breaks alongside hydration reminders."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _lookup_external_note(prompt: str) -> str | None:
|
||||
"""Return the first matching external note based on a keyword search."""
|
||||
lowered = prompt.lower()
|
||||
for keyword, note in EXTERNAL_REFERENCES.items():
|
||||
if keyword in lowered:
|
||||
return note
|
||||
return None
|
||||
|
||||
|
||||
@executor(id="enrich_with_references")
|
||||
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."""
|
||||
conversation = list(draft.full_conversation or draft.agent_response.messages)
|
||||
original_prompt = next((message.text for message in conversation if message.role == Role.USER), "")
|
||||
external_note = _lookup_external_note(original_prompt) or (
|
||||
"No additional references were found. Please refine the previous assistant response for clarity."
|
||||
)
|
||||
|
||||
follow_up = (
|
||||
"External knowledge snippet:\n"
|
||||
f"{external_note}\n\n"
|
||||
"Please update the prior assistant answer so it weaves this note into the guidance."
|
||||
)
|
||||
conversation.append(ChatMessage(role=Role.USER, text=follow_up))
|
||||
|
||||
await ctx.send_message(AgentExecutorRequest(messages=conversation))
|
||||
|
||||
|
||||
def create_research_agent():
|
||||
return 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(
|
||||
name="final_editor_agent",
|
||||
instructions=(
|
||||
"Use all conversation context (including external notes) to produce the final answer. "
|
||||
"Merge the draft and extra note into a concise recommendation under 150 words."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
.build()
|
||||
)
|
||||
|
||||
events = workflow.run_stream(
|
||||
"Create quick workspace wellness tips for a remote analyst working across two monitors."
|
||||
)
|
||||
|
||||
last_executor: 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)")
|
||||
else:
|
||||
print(response if response is not None else "No response generated.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: 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.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
def create_writer_agent():
|
||||
return 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(
|
||||
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."
|
||||
),
|
||||
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()
|
||||
)
|
||||
|
||||
# Stream events from the workflow. We aggregate partial token updates per executor for readable output.
|
||||
last_executor_id: 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."
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,321 @@
|
||||
# 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,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
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}'.")
|
||||
|
||||
|
||||
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(Role.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(Role.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())
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import ConcurrentBuilder
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Build a concurrent workflow orchestration and wrap it as an agent.
|
||||
|
||||
This script wires up a fan-out/fan-in workflow using `ConcurrentBuilder`, and then
|
||||
invokes the entire orchestration through the `workflow.as_agent(...)` interface so
|
||||
downstream coordinators can reuse the orchestration as a single agent.
|
||||
|
||||
Demonstrates:
|
||||
- Fan-out to multiple agents, fan-in aggregation of final ChatMessages.
|
||||
- Reusing the orchestrated workflow as an agent entry point with `workflow.as_agent(...)`.
|
||||
- Workflow completion when idle with no pending work
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
|
||||
- Familiarity with Workflow events (AgentRunEvent, WorkflowOutputEvent)
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 1) Create three domain agents using AzureOpenAIChatClient
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
researcher = chat_client.as_agent(
|
||||
instructions=(
|
||||
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
|
||||
" opportunities, and risks."
|
||||
),
|
||||
name="researcher",
|
||||
)
|
||||
|
||||
marketer = chat_client.as_agent(
|
||||
instructions=(
|
||||
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
|
||||
" aligned to the prompt."
|
||||
),
|
||||
name="marketer",
|
||||
)
|
||||
|
||||
legal = chat_client.as_agent(
|
||||
instructions=(
|
||||
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
|
||||
" based on the prompt."
|
||||
),
|
||||
name="legal",
|
||||
)
|
||||
|
||||
# 2) Build a concurrent workflow
|
||||
workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build()
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,132 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Step 2: Agents in a Workflow non-streaming
|
||||
|
||||
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.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming or non streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
class Writer(Executor):
|
||||
"""Custom executor that owns a domain specific agent responsible for generating content.
|
||||
|
||||
This class demonstrates:
|
||||
- Attaching a ChatAgent to an Executor so it participates as a node in a workflow.
|
||||
- Using a @handler method to accept a typed input and forward a typed output via ctx.send_message.
|
||||
"""
|
||||
|
||||
agent: ChatAgent
|
||||
|
||||
def __init__(self, id: str = "writer"):
|
||||
# Create a domain specific agent using your configured AzureOpenAIChatClient.
|
||||
self.agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
)
|
||||
# Associate the agent with this executor node. The base Executor stores it on self.agent.
|
||||
super().__init__(id=id)
|
||||
|
||||
@handler
|
||||
async def handle(self, message: ChatMessage, ctx: WorkflowContext[list[ChatMessage], str]) -> None:
|
||||
"""Generate content using the agent and forward the updated conversation.
|
||||
|
||||
Contract for this handler:
|
||||
- message is the inbound user ChatMessage.
|
||||
- ctx is a WorkflowContext that expects a list[ChatMessage] to be sent downstream.
|
||||
|
||||
Pattern shown here:
|
||||
1) Seed the conversation with the inbound message.
|
||||
2) Run the attached agent to produce assistant messages.
|
||||
3) Forward the cumulative messages to the next executor with ctx.send_message.
|
||||
"""
|
||||
# Start the conversation with the incoming user message.
|
||||
messages: list[ChatMessage] = [message]
|
||||
# Run the agent and extend the conversation with the agent's messages.
|
||||
response = await self.agent.run(messages)
|
||||
messages.extend(response.messages)
|
||||
# Forward the accumulated messages to the next executor in the workflow.
|
||||
await ctx.send_message(messages)
|
||||
|
||||
|
||||
class Reviewer(Executor):
|
||||
"""Custom executor that owns a review agent and completes the workflow.
|
||||
|
||||
This class demonstrates:
|
||||
- Consuming a typed payload produced upstream.
|
||||
- Yielding the final text outcome to complete the workflow.
|
||||
"""
|
||||
|
||||
agent: ChatAgent
|
||||
|
||||
def __init__(self, id: str = "reviewer"):
|
||||
# Create a domain specific agent that evaluates and refines content.
|
||||
self.agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
instructions=(
|
||||
"You are an excellent content reviewer. You review the content and provide feedback to the writer."
|
||||
),
|
||||
)
|
||||
super().__init__(id=id)
|
||||
|
||||
@handler
|
||||
async def handle(self, messages: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage], str]) -> None:
|
||||
"""Review the full conversation transcript and complete with a final string.
|
||||
|
||||
This node consumes all messages so far. It uses its agent to produce the final text,
|
||||
then signals completion by yielding the output.
|
||||
"""
|
||||
response = await self.agent.run(messages)
|
||||
await ctx.yield_output(response.text)
|
||||
|
||||
|
||||
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.
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.register_executor(Writer, name="writer")
|
||||
.register_executor(Reviewer, name="reviewer")
|
||||
.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.
|
||||
events = await workflow.run(
|
||||
ChatMessage(role="user", text="Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
)
|
||||
# The terminal node yields output; print its contents.
|
||||
outputs = events.get_outputs()
|
||||
if outputs:
|
||||
print(outputs[-1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import ChatAgent, GroupChatBuilder
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
|
||||
|
||||
"""
|
||||
Sample: Group Chat Orchestration
|
||||
|
||||
What it does:
|
||||
- Demonstrates the generic GroupChatBuilder with a agent orchestrator directing two agents.
|
||||
- The orchestrator coordinates a researcher (chat completions) and a writer (responses API) to solve a task.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI environment variables configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
researcher = ChatAgent(
|
||||
name="Researcher",
|
||||
description="Collects relevant background information.",
|
||||
instructions="Gather concise facts that help a teammate answer the question.",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o-mini"),
|
||||
)
|
||||
|
||||
writer = ChatAgent(
|
||||
name="Writer",
|
||||
description="Synthesizes a polished answer using the gathered notes.",
|
||||
instructions="Compose clear and structured answers using any notes provided.",
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
)
|
||||
|
||||
workflow = (
|
||||
GroupChatBuilder()
|
||||
.with_agent_orchestrator(
|
||||
OpenAIChatClient().as_agent(
|
||||
name="Orchestrator",
|
||||
instructions="You coordinate a team conversation to solve the user's task.",
|
||||
)
|
||||
)
|
||||
.participants([researcher, writer])
|
||||
.build()
|
||||
)
|
||||
|
||||
task = "Outline the core considerations for planning a community hackathon, and finish with a concise action plan."
|
||||
|
||||
print("\nStarting Group Chat Workflow...\n")
|
||||
print(f"Input: {task}\n")
|
||||
|
||||
try:
|
||||
workflow_agent = workflow.as_agent(name="GroupChatWorkflowAgent")
|
||||
agent_result = await workflow_agent.run(task)
|
||||
|
||||
if agent_result.messages:
|
||||
print("\n===== as_agent() Transcript =====")
|
||||
for i, msg in enumerate(agent_result.messages, start=1):
|
||||
role_value = getattr(msg.role, "value", msg.role)
|
||||
speaker = msg.author_name or role_value
|
||||
print(f"{'-' * 50}\n{i:02d} [{speaker}]\n{msg.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Workflow execution failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,224 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
HandoffAgentUserRequest,
|
||||
HandoffBuilder,
|
||||
Role,
|
||||
WorkflowAgent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""Sample: Handoff Workflow as Agent with Human-in-the-Loop.
|
||||
|
||||
This sample demonstrates how to use a handoff workflow as an agent, enabling
|
||||
human-in-the-loop interactions through the agent interface.
|
||||
|
||||
A handoff workflow defines a pattern that assembles agents in a mesh topology, allowing
|
||||
them to transfer control to each other based on the conversation context.
|
||||
|
||||
Prerequisites:
|
||||
- `az login` (Azure CLI authentication)
|
||||
- Environment variables configured for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
|
||||
|
||||
Key Concepts:
|
||||
- Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools
|
||||
for each participant, allowing the coordinator to transfer control to specialists
|
||||
- Termination condition: Controls when the workflow stops requesting user input
|
||||
- Request/response cycle: Workflow requests input, user responds, cycle continues
|
||||
"""
|
||||
|
||||
|
||||
@ai_function
|
||||
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."""
|
||||
return f"Refund processed successfully for order {order_number}."
|
||||
|
||||
|
||||
@ai_function
|
||||
def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str:
|
||||
"""Simulated function to check the status of a given order number."""
|
||||
return f"Order {order_number} is currently being processed and will ship in 2 business days."
|
||||
|
||||
|
||||
@ai_function
|
||||
def process_return(order_number: Annotated[str, "Order number to process return for"]) -> str:
|
||||
"""Simulated function to process a return for a given order number."""
|
||||
return f"Return initiated successfully for order {order_number}. You will receive return instructions via email."
|
||||
|
||||
|
||||
def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAgent, ChatAgent, ChatAgent]:
|
||||
"""Create and configure the triage and specialist agents.
|
||||
|
||||
Args:
|
||||
chat_client: The AzureOpenAIChatClient to use for creating agents.
|
||||
|
||||
Returns:
|
||||
Tuple of (triage_agent, refund_agent, order_agent, return_agent)
|
||||
"""
|
||||
# Triage agent: Acts as the frontline dispatcher
|
||||
triage_agent = chat_client.as_agent(
|
||||
instructions=(
|
||||
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
|
||||
"based on the problem described."
|
||||
),
|
||||
name="triage_agent",
|
||||
)
|
||||
|
||||
# Refund specialist: Handles refund requests
|
||||
refund_agent = chat_client.as_agent(
|
||||
instructions="You process refund requests.",
|
||||
name="refund_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[process_refund],
|
||||
)
|
||||
|
||||
# Order/shipping specialist: Resolves delivery issues
|
||||
order_agent = chat_client.as_agent(
|
||||
instructions="You handle order and shipping inquiries.",
|
||||
name="order_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[check_order_status],
|
||||
)
|
||||
|
||||
# Return specialist: Handles return requests
|
||||
return_agent = chat_client.as_agent(
|
||||
instructions="You manage product return requests.",
|
||||
name="return_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[process_return],
|
||||
)
|
||||
|
||||
return triage_agent, refund_agent, order_agent, return_agent
|
||||
|
||||
|
||||
def handle_response_and_requests(response: AgentResponse) -> dict[str, HandoffAgentUserRequest]:
|
||||
"""Process agent response messages and extract any user requests.
|
||||
|
||||
This function inspects the agent response and:
|
||||
- Displays agent messages to the console
|
||||
- Collects HandoffAgentUserRequest instances for response handling
|
||||
|
||||
Args:
|
||||
response: The AgentResponse from the agent run call.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping request IDs to HandoffAgentUserRequest instances.
|
||||
"""
|
||||
pending_requests: dict[str, HandoffAgentUserRequest] = {}
|
||||
for message in response.messages:
|
||||
if message.text:
|
||||
print(f"- {message.author_name or message.role.value}: {message.text}")
|
||||
for content in message.contents:
|
||||
if isinstance(content, FunctionCallContent):
|
||||
if isinstance(content.arguments, dict):
|
||||
request = WorkflowAgent.RequestInfoFunctionArgs.from_dict(content.arguments)
|
||||
elif isinstance(content.arguments, str):
|
||||
request = WorkflowAgent.RequestInfoFunctionArgs.from_json(content.arguments)
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main entry point for the handoff workflow demo.
|
||||
|
||||
This function demonstrates:
|
||||
1. Creating triage and specialist agents
|
||||
2. Building a handoff workflow with custom termination condition
|
||||
3. Running the workflow with scripted user responses
|
||||
4. Processing events and handling user input requests
|
||||
|
||||
The workflow uses scripted responses instead of interactive input to make
|
||||
the demo reproducible and testable. In a production application, you would
|
||||
replace the scripted_responses with actual user input collection.
|
||||
"""
|
||||
# Initialize the Azure OpenAI chat client
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Create all agents: triage + specialists
|
||||
triage, refund, order, support = create_agents(chat_client)
|
||||
|
||||
# Build the handoff workflow
|
||||
# - participants: All agents that can participate in the workflow
|
||||
# - with_start_agent: The triage agent is designated as the start agent, which means
|
||||
# it receives all user input first and orchestrates handoffs to specialists
|
||||
# - with_termination_condition: Custom logic to stop the request/response loop.
|
||||
# Without this, the default behavior continues requesting user input until max_turns
|
||||
# is reached. Here we use a custom condition that checks if the conversation has ended
|
||||
# naturally (when one of the agents says something like "you're welcome").
|
||||
agent = (
|
||||
HandoffBuilder(
|
||||
name="customer_support_handoff",
|
||||
participants=[triage, refund, order, support],
|
||||
)
|
||||
.with_start_agent(triage)
|
||||
.with_termination_condition(
|
||||
# Custom termination: Check if one of the agents has provided a closing message.
|
||||
# This looks for the last message containing "welcome", which indicates the
|
||||
# conversation has concluded naturally.
|
||||
lambda conversation: len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
|
||||
)
|
||||
.build()
|
||||
.as_agent() # Convert workflow to agent interface
|
||||
)
|
||||
|
||||
# Scripted user responses for reproducible demo
|
||||
# In a console application, replace this with:
|
||||
# user_input = input("Your response: ")
|
||||
# or integrate with a UI/chat interface
|
||||
scripted_responses = [
|
||||
"My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.",
|
||||
"Please also process a refund for order 1234.",
|
||||
"Thanks for resolving this.",
|
||||
]
|
||||
|
||||
# Start the workflow with the initial user message
|
||||
print("[Starting workflow with initial user message...]\n")
|
||||
initial_message = "Hello, I need assistance with my recent purchase."
|
||||
print(f"- User: {initial_message}")
|
||||
response = await agent.run(initial_message)
|
||||
pending_requests = handle_response_and_requests(response)
|
||||
|
||||
# Process the request/response cycle
|
||||
# The workflow will continue requesting input until:
|
||||
# 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.value}: {message.text}")
|
||||
|
||||
if not scripted_responses:
|
||||
# No more scripted responses; terminate the workflow
|
||||
responses = {req_id: HandoffAgentUserRequest.terminate() for req_id in pending_requests}
|
||||
else:
|
||||
# Get the next scripted response
|
||||
user_response = scripted_responses.pop(0)
|
||||
print(f"\n- User: {user_response}")
|
||||
|
||||
# Send response(s) to all pending requests
|
||||
# In this demo, there's typically one request per cycle, but the API supports multiple
|
||||
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()
|
||||
]
|
||||
response = await agent.run(ChatMessage(role=Role.TOOL, contents=function_results))
|
||||
pending_requests = handle_response_and_requests(response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,92 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
HostedCodeInterpreterTool,
|
||||
MagenticBuilder,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
|
||||
|
||||
"""
|
||||
Sample: Build a Magentic orchestration and wrap it as an agent.
|
||||
|
||||
The script configures a Magentic workflow with streaming callbacks, then invokes the
|
||||
orchestration through `workflow.as_agent(...)` so the entire Magentic loop can be reused
|
||||
like any other agent while still emitting callback telemetry.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI credentials configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
researcher_agent = ChatAgent(
|
||||
name="ResearcherAgent",
|
||||
description="Specialist in research and information gathering",
|
||||
instructions=(
|
||||
"You are a Researcher. You find information without additional computation or quantitative analysis."
|
||||
),
|
||||
# This agent requires the gpt-4o-search-preview model to perform web searches.
|
||||
# Feel free to explore with other agents that support web search, for example,
|
||||
# the `OpenAIResponseAgent` or `AzureAgentProtocol` with bing grounding.
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
|
||||
)
|
||||
|
||||
coder_agent = ChatAgent(
|
||||
name="CoderAgent",
|
||||
description="A helpful assistant that writes and executes code to process and analyze data.",
|
||||
instructions="You solve questions using code. Please provide detailed analysis and computation process.",
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
)
|
||||
|
||||
# Create a manager agent for orchestration
|
||||
manager_agent = ChatAgent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the research and coding workflow",
|
||||
instructions="You coordinate a team to complete complex tasks efficiently.",
|
||||
chat_client=OpenAIChatClient(),
|
||||
)
|
||||
|
||||
print("\nBuilding Magentic Workflow...")
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([researcher_agent, coder_agent])
|
||||
.with_standard_manager(
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=3,
|
||||
max_reset_count=2,
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
task = (
|
||||
"I am preparing a report on the energy efficiency of different machine learning model architectures. "
|
||||
"Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 "
|
||||
"on standard datasets (e.g., ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). "
|
||||
"Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 "
|
||||
"VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model "
|
||||
"per task type (image classification, text classification, and text generation)."
|
||||
)
|
||||
|
||||
print(f"\nTask: {task}")
|
||||
print("\nStarting workflow execution...")
|
||||
|
||||
try:
|
||||
# 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):
|
||||
# Fallback for any other events with text
|
||||
print(response.text, end="", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Workflow execution failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,122 @@
|
||||
# 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())
|
||||
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Role, SequentialBuilder
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Build a sequential workflow orchestration and wrap it as an agent.
|
||||
|
||||
The script assembles a sequential conversation flow with `SequentialBuilder`, then
|
||||
invokes the entire orchestration through the `workflow.as_agent(...)` interface so
|
||||
other coordinators can reuse the chain as a single participant.
|
||||
|
||||
Note on internal adapters:
|
||||
- Sequential orchestration includes small adapter nodes for input normalization
|
||||
("input-conversation"), agent-response conversion ("to-conversation:<participant>"),
|
||||
and completion ("complete"). These may appear as ExecutorInvoke/Completed events in
|
||||
the stream—similar to how concurrent orchestration includes a dispatcher/aggregator.
|
||||
You can safely ignore them when focusing on agent progress.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 1) Create agents
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
writer = chat_client.as_agent(
|
||||
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
|
||||
name="writer",
|
||||
)
|
||||
|
||||
reviewer = chat_client.as_agent(
|
||||
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
|
||||
name="reviewer",
|
||||
)
|
||||
|
||||
# 2) Build sequential workflow: writer -> reviewer
|
||||
workflow = SequentialBuilder().participants([writer, reviewer]).build()
|
||||
|
||||
# 3) Treat the workflow itself as an agent for follow-up invocations
|
||||
agent = workflow.as_agent(name="SequentialWorkflowAgent")
|
||||
prompt = "Write a tagline for a budget-friendly eBike."
|
||||
agent_response = await agent.run(prompt)
|
||||
|
||||
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 == Role.ASSISTANT.value else "user")
|
||||
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
===== Final Conversation =====
|
||||
------------------------------------------------------------
|
||||
01 [user]
|
||||
Write a tagline for a budget-friendly eBike.
|
||||
------------------------------------------------------------
|
||||
02 [writer]
|
||||
Ride farther, spend less—your affordable eBike adventure starts here.
|
||||
------------------------------------------------------------
|
||||
03 [reviewer]
|
||||
This tagline clearly communicates affordability and the benefit of extended travel, making it
|
||||
appealing to budget-conscious consumers. It has a friendly and motivating tone, though it could
|
||||
be slightly shorter for more punch. Overall, a strong and effective suggestion!
|
||||
|
||||
===== as_agent() Conversation =====
|
||||
------------------------------------------------------------
|
||||
01 [writer]
|
||||
Go electric, save big—your affordable ride awaits!
|
||||
------------------------------------------------------------
|
||||
02 [reviewer]
|
||||
Catchy and straightforward! The tagline clearly emphasizes both the electric aspect and the affordability of the
|
||||
eBike. It's inviting and actionable. For even more impact, consider making it slightly shorter:
|
||||
"Go electric, save big." Overall, this is an effective and appealing suggestion for a budget-friendly eBike.
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,179 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
# Ensure local getting_started package can be imported when running as a script.
|
||||
_SAMPLES_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(_SAMPLES_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_SAMPLES_ROOT))
|
||||
|
||||
from agent_framework import ( # noqa: E402
|
||||
ChatMessage,
|
||||
Executor,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
WorkflowAgent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
from getting_started.workflows.agents.workflow_as_agent_reflection_pattern import ( # noqa: E402
|
||||
ReviewRequest,
|
||||
ReviewResponse,
|
||||
Worker,
|
||||
)
|
||||
|
||||
"""
|
||||
Sample: Workflow Agent with Human-in-the-Loop
|
||||
|
||||
Purpose:
|
||||
This sample demonstrates how to build a workflow agent that escalates uncertain
|
||||
decisions to a human manager. A Worker generates results, while a Reviewer
|
||||
evaluates them. When the Reviewer is not confident, it escalates the decision
|
||||
to a human, receives the human response, and then forwards that response back
|
||||
to the Worker. The workflow completes when idle.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI account configured and accessible for OpenAIChatClient.
|
||||
- Familiarity with WorkflowBuilder, Executor, and WorkflowContext from agent_framework.
|
||||
- Understanding of request-response message handling in executors.
|
||||
- (Optional) Review of reflection and escalation patterns, such as those in
|
||||
workflow_as_agent_reflection.py.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class HumanReviewRequest:
|
||||
"""A request message type for escalation to a human reviewer."""
|
||||
|
||||
agent_request: ReviewRequest | None = None
|
||||
|
||||
|
||||
class ReviewerWithHumanInTheLoop(Executor):
|
||||
"""Executor that always escalates reviews to a human manager."""
|
||||
|
||||
def __init__(self, worker_id: str, reviewer_id: str | None = None) -> None:
|
||||
unique_id = reviewer_id or f"{worker_id}-reviewer"
|
||||
super().__init__(id=unique_id)
|
||||
self._worker_id = worker_id
|
||||
|
||||
@handler
|
||||
async def review(self, request: ReviewRequest, ctx: WorkflowContext) -> None:
|
||||
# In this simplified example, we always escalate to a human manager.
|
||||
# See workflow_as_agent_reflection.py for an implementation
|
||||
# using an automated agent to make the review decision.
|
||||
print(f"Reviewer: Evaluating response for request {request.request_id[:8]}...")
|
||||
print("Reviewer: Escalating to human manager...")
|
||||
|
||||
# Forward the request to a human manager by sending a HumanReviewRequest.
|
||||
await ctx.request_info(request_data=HumanReviewRequest(agent_request=request), response_type=ReviewResponse)
|
||||
|
||||
@response_handler
|
||||
async def accept_human_review(
|
||||
self,
|
||||
original_request: HumanReviewRequest,
|
||||
response: ReviewResponse,
|
||||
ctx: WorkflowContext[ReviewResponse],
|
||||
) -> None:
|
||||
# Accept the human review response and forward it back to the Worker.
|
||||
print(f"Reviewer: Accepting human review for request {response.request_id[:8]}...")
|
||||
print(f"Reviewer: Human feedback: {response.feedback}")
|
||||
print(f"Reviewer: Human approved: {response.approved}")
|
||||
print("Reviewer: Forwarding human review back to worker...")
|
||||
await ctx.send_message(response, target_id=self._worker_id)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("Starting Workflow Agent with Human-in-the-Loop Demo")
|
||||
print("=" * 50)
|
||||
|
||||
print("Building workflow with Worker-Reviewer cycle...")
|
||||
# Build a workflow with bidirectional communication between Worker and Reviewer,
|
||||
# and escalation paths for human review.
|
||||
agent = (
|
||||
WorkflowBuilder()
|
||||
.register_executor(
|
||||
lambda: Worker(
|
||||
id="sub-worker",
|
||||
chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
|
||||
),
|
||||
name="worker",
|
||||
)
|
||||
.register_executor(
|
||||
lambda: ReviewerWithHumanInTheLoop(worker_id="sub-worker"),
|
||||
name="reviewer",
|
||||
)
|
||||
.add_edge("worker", "reviewer") # Worker sends requests to Reviewer
|
||||
.add_edge("reviewer", "worker") # Reviewer sends feedback to Worker
|
||||
.set_start_executor("worker")
|
||||
.build()
|
||||
.as_agent() # Convert workflow into an agent interface
|
||||
)
|
||||
|
||||
print("Running workflow agent with user query...")
|
||||
print("Query: 'Write code for parallel reading 1 million files on disk and write to a sorted output file.'")
|
||||
print("-" * 50)
|
||||
|
||||
# Run the agent with an initial query.
|
||||
response = await agent.run(
|
||||
"Write code for parallel reading 1 million Files on disk and write to a sorted output file."
|
||||
)
|
||||
|
||||
# Locate the human review function call in the response messages.
|
||||
human_review_function_call: FunctionCallContent | None = None
|
||||
for message in response.messages:
|
||||
for content in message.contents:
|
||||
if isinstance(content, FunctionCallContent) and content.name == WorkflowAgent.REQUEST_INFO_FUNCTION_NAME:
|
||||
human_review_function_call = content
|
||||
|
||||
# Handle the human review if required.
|
||||
if human_review_function_call:
|
||||
# Parse the human review request arguments.
|
||||
human_request_args = human_review_function_call.arguments
|
||||
if isinstance(human_request_args, str):
|
||||
request: WorkflowAgent.RequestInfoFunctionArgs = WorkflowAgent.RequestInfoFunctionArgs.from_json(
|
||||
human_request_args
|
||||
)
|
||||
elif isinstance(human_request_args, Mapping):
|
||||
request = WorkflowAgent.RequestInfoFunctionArgs.from_dict(dict(human_request_args))
|
||||
else:
|
||||
raise TypeError("Unexpected argument type for human review function call.")
|
||||
|
||||
request_payload: Any = request.data
|
||||
if not isinstance(request_payload, HumanReviewRequest):
|
||||
raise ValueError("Human review request payload must be a HumanReviewRequest.")
|
||||
|
||||
agent_request = request_payload.agent_request
|
||||
if agent_request is None:
|
||||
raise ValueError("Human review request must include agent_request.")
|
||||
|
||||
request_id = agent_request.request_id
|
||||
# Mock a human response approval for demonstration purposes.
|
||||
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,
|
||||
result=human_response,
|
||||
)
|
||||
# Send the human review result back to the agent.
|
||||
response = await agent.run(ChatMessage(role=Role.TOOL, contents=[human_review_function_result]))
|
||||
print(f"📤 Agent Response: {response.messages[-1].text}")
|
||||
|
||||
print("=" * 50)
|
||||
print("Workflow completed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Initializing Workflow as Agent Sample...")
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,140 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Annotated, Any
|
||||
|
||||
from agent_framework import SequentialBuilder, ai_function
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with kwargs Propagation to @ai_function Tools
|
||||
|
||||
This sample demonstrates how to flow custom context (skill data, user tokens, etc.)
|
||||
through a workflow exposed via .as_agent() to @ai_function tools using the **kwargs pattern.
|
||||
|
||||
Key Concepts:
|
||||
- Build a workflow using SequentialBuilder (or any builder pattern)
|
||||
- Expose the workflow as a reusable agent via workflow.as_agent()
|
||||
- Pass custom context as kwargs when invoking workflow_agent.run() or run_stream()
|
||||
- kwargs are stored in SharedState and propagated to all agent invocations
|
||||
- @ai_function tools receive kwargs via **kwargs parameter
|
||||
|
||||
When to use workflow.as_agent():
|
||||
- To treat an entire workflow orchestration as a single agent
|
||||
- To compose workflows into higher-level orchestrations
|
||||
- To maintain a consistent agent interface for callers
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI environment variables configured
|
||||
"""
|
||||
|
||||
|
||||
# Define tools that accept custom context via **kwargs
|
||||
@ai_function
|
||||
def get_user_data(
|
||||
query: Annotated[str, Field(description="What user data to retrieve")],
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Retrieve user-specific data based on the authenticated context."""
|
||||
user_token = kwargs.get("user_token", {})
|
||||
user_name = user_token.get("user_name", "anonymous")
|
||||
access_level = user_token.get("access_level", "none")
|
||||
|
||||
print(f"\n[get_user_data] Received kwargs keys: {list(kwargs.keys())}")
|
||||
print(f"[get_user_data] User: {user_name}")
|
||||
print(f"[get_user_data] Access level: {access_level}")
|
||||
|
||||
return f"Retrieved data for user {user_name} with {access_level} access: {query}"
|
||||
|
||||
|
||||
@ai_function
|
||||
def call_api(
|
||||
endpoint_name: Annotated[str, Field(description="Name of the API endpoint to call")],
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Call an API using the configured endpoints from custom_data."""
|
||||
custom_data = kwargs.get("custom_data", {})
|
||||
api_config = custom_data.get("api_config", {})
|
||||
|
||||
base_url = api_config.get("base_url", "unknown")
|
||||
endpoints = api_config.get("endpoints", {})
|
||||
|
||||
print(f"\n[call_api] Received kwargs keys: {list(kwargs.keys())}")
|
||||
print(f"[call_api] Base URL: {base_url}")
|
||||
print(f"[call_api] Available endpoints: {list(endpoints.keys())}")
|
||||
|
||||
if endpoint_name in endpoints:
|
||||
return f"Called {base_url}{endpoints[endpoint_name]} successfully"
|
||||
return f"Endpoint '{endpoint_name}' not found in configuration"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=" * 70)
|
||||
print("Workflow as Agent kwargs Flow Demo")
|
||||
print("=" * 70)
|
||||
|
||||
# Create chat client
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
# Create agent with tools that use kwargs
|
||||
agent = chat_client.as_agent(
|
||||
name="assistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Use the available tools to help users. "
|
||||
"When asked about user data, use get_user_data. "
|
||||
"When asked to call an API, use call_api."
|
||||
),
|
||||
tools=[get_user_data, call_api],
|
||||
)
|
||||
|
||||
# Build a sequential workflow
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
|
||||
# Expose the workflow as an agent using .as_agent()
|
||||
workflow_agent = workflow.as_agent(name="WorkflowAgent")
|
||||
|
||||
# Define custom context that will flow to ai_functions via kwargs
|
||||
custom_data = {
|
||||
"api_config": {
|
||||
"base_url": "https://api.example.com",
|
||||
"endpoints": {
|
||||
"users": "/v1/users",
|
||||
"orders": "/v1/orders",
|
||||
"products": "/v1/products",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
user_token = {
|
||||
"user_name": "bob@contoso.com",
|
||||
"access_level": "admin",
|
||||
}
|
||||
|
||||
print("\nCustom Data being passed:")
|
||||
print(json.dumps(custom_data, indent=2))
|
||||
print(f"\nUser: {user_token['user_name']}")
|
||||
print("\n" + "-" * 70)
|
||||
print("Workflow Agent Execution (watch for [tool_name] logs showing kwargs received):")
|
||||
print("-" * 70)
|
||||
|
||||
# Run workflow agent with kwargs - these will flow through to ai_functions
|
||||
# Note: kwargs are passed to workflow_agent.run_stream() just like workflow.run_stream()
|
||||
print("\n===== Streaming Response =====")
|
||||
async for update in workflow_agent.run_stream(
|
||||
"Please get my user data and then call the users API endpoint.",
|
||||
custom_data=custom_data,
|
||||
user_token=user_token,
|
||||
):
|
||||
if update.text:
|
||||
print(update.text, end="", flush=True)
|
||||
print()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Sample Complete")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,232 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
AgentRunUpdateEvent,
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
Content,
|
||||
Executor,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with Reflection and Retry Pattern
|
||||
|
||||
Purpose:
|
||||
This sample demonstrates how to wrap a workflow as an agent using WorkflowAgent.
|
||||
It uses a reflection pattern where a Worker executor generates responses and a
|
||||
Reviewer executor evaluates them. If the response is not approved, the Worker
|
||||
regenerates the output based on feedback until the Reviewer approves it. Only
|
||||
approved responses are emitted to the external consumer. The workflow completes when idle.
|
||||
|
||||
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.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI account configured and accessible for OpenAIChatClient.
|
||||
- Familiarity with WorkflowBuilder, Executor, WorkflowContext, and event handling.
|
||||
- Understanding of how agent messages are generated, reviewed, and re-submitted.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReviewRequest:
|
||||
"""Structured request passed from Worker to Reviewer for evaluation."""
|
||||
|
||||
request_id: str
|
||||
user_messages: list[ChatMessage]
|
||||
agent_messages: list[ChatMessage]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReviewResponse:
|
||||
"""Structured response from Reviewer back to Worker."""
|
||||
|
||||
request_id: str
|
||||
feedback: str
|
||||
approved: bool
|
||||
|
||||
|
||||
class Reviewer(Executor):
|
||||
"""Executor that reviews agent responses and provides structured feedback."""
|
||||
|
||||
def __init__(self, id: str, chat_client: ChatClientProtocol) -> None:
|
||||
super().__init__(id=id)
|
||||
self._chat_client = chat_client
|
||||
|
||||
@handler
|
||||
async def review(self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse]) -> None:
|
||||
print(f"Reviewer: Evaluating response for request {request.request_id[:8]}...")
|
||||
|
||||
# Define structured schema for the LLM to return.
|
||||
class _Response(BaseModel):
|
||||
feedback: str
|
||||
approved: bool
|
||||
|
||||
# Construct review instructions and context.
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role=Role.SYSTEM,
|
||||
text=(
|
||||
"You are a reviewer for an AI agent. Provide feedback on the "
|
||||
"exchange between a user and the agent. Indicate approval only if:\n"
|
||||
"- Relevance: response addresses the query\n"
|
||||
"- Accuracy: information is correct\n"
|
||||
"- Clarity: response is easy to understand\n"
|
||||
"- Completeness: response covers all aspects\n"
|
||||
"Do not approve until all criteria are satisfied."
|
||||
),
|
||||
)
|
||||
]
|
||||
# Add conversation history.
|
||||
messages.extend(request.user_messages)
|
||||
messages.extend(request.agent_messages)
|
||||
|
||||
# Add explicit review instruction.
|
||||
messages.append(ChatMessage(role=Role.USER, text="Please review the agent's responses."))
|
||||
|
||||
print("Reviewer: Sending review request to LLM...")
|
||||
response = await self._chat_client.get_response(messages=messages, options={"response_format": _Response})
|
||||
|
||||
parsed = _Response.model_validate_json(response.messages[-1].text)
|
||||
|
||||
print(f"Reviewer: Review complete - Approved: {parsed.approved}")
|
||||
print(f"Reviewer: Feedback: {parsed.feedback}")
|
||||
|
||||
# Send structured review result to Worker.
|
||||
await ctx.send_message(
|
||||
ReviewResponse(request_id=request.request_id, feedback=parsed.feedback, approved=parsed.approved)
|
||||
)
|
||||
|
||||
|
||||
class Worker(Executor):
|
||||
"""Executor that generates responses and incorporates feedback when necessary."""
|
||||
|
||||
def __init__(self, id: str, chat_client: ChatClientProtocol) -> None:
|
||||
super().__init__(id=id)
|
||||
self._chat_client = chat_client
|
||||
self._pending_requests: dict[str, tuple[ReviewRequest, list[ChatMessage]]] = {}
|
||||
|
||||
@handler
|
||||
async def handle_user_messages(self, user_messages: list[ChatMessage], ctx: WorkflowContext[ReviewRequest]) -> None:
|
||||
print("Worker: Received user messages, generating response...")
|
||||
|
||||
# Initialize chat with system prompt.
|
||||
messages = [ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant.")]
|
||||
messages.extend(user_messages)
|
||||
|
||||
print("Worker: Calling LLM to generate response...")
|
||||
response = await self._chat_client.get_response(messages=messages)
|
||||
print(f"Worker: Response generated: {response.messages[-1].text}")
|
||||
|
||||
# Add agent messages to context.
|
||||
messages.extend(response.messages)
|
||||
|
||||
# Create review request and send to Reviewer.
|
||||
request = ReviewRequest(request_id=str(uuid4()), user_messages=user_messages, agent_messages=response.messages)
|
||||
print(f"Worker: Sending response for review (ID: {request.request_id[:8]})")
|
||||
await ctx.send_message(request)
|
||||
|
||||
# Track request for possible retry.
|
||||
self._pending_requests[request.request_id] = (request, messages)
|
||||
|
||||
@handler
|
||||
async def handle_review_response(self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest]) -> None:
|
||||
print(f"Worker: Received review for request {review.request_id[:8]} - Approved: {review.approved}")
|
||||
|
||||
if review.request_id not in self._pending_requests:
|
||||
raise ValueError(f"Unknown request ID in review: {review.request_id}")
|
||||
|
||||
request, messages = self._pending_requests.pop(review.request_id)
|
||||
|
||||
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=Role.ASSISTANT))
|
||||
)
|
||||
return
|
||||
|
||||
print(f"Worker: Response not approved. Feedback: {review.feedback}")
|
||||
print("Worker: Regenerating response with feedback...")
|
||||
|
||||
# Incorporate review feedback.
|
||||
messages.append(ChatMessage(role=Role.SYSTEM, text=review.feedback))
|
||||
messages.append(
|
||||
ChatMessage(role=Role.SYSTEM, text="Please incorporate the feedback and regenerate the response.")
|
||||
)
|
||||
messages.extend(request.user_messages)
|
||||
|
||||
# Retry with updated prompt.
|
||||
response = await self._chat_client.get_response(messages=messages)
|
||||
print(f"Worker: New response generated: {response.messages[-1].text}")
|
||||
|
||||
messages.extend(response.messages)
|
||||
|
||||
# Send updated request for re-review.
|
||||
new_request = ReviewRequest(
|
||||
request_id=review.request_id, user_messages=request.user_messages, agent_messages=response.messages
|
||||
)
|
||||
await ctx.send_message(new_request)
|
||||
|
||||
# Track new request for further evaluation.
|
||||
self._pending_requests[new_request.request_id] = (new_request, messages)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("Starting Workflow Agent Demo")
|
||||
print("=" * 50)
|
||||
|
||||
print("Building workflow with Worker ↔ Reviewer cycle...")
|
||||
agent = (
|
||||
WorkflowBuilder()
|
||||
.register_executor(
|
||||
lambda: Worker(id="worker", chat_client=OpenAIChatClient(model_id="gpt-4.1-nano")),
|
||||
name="worker",
|
||||
)
|
||||
.register_executor(
|
||||
lambda: Reviewer(id="reviewer", chat_client=OpenAIChatClient(model_id="gpt-4.1")),
|
||||
name="reviewer",
|
||||
)
|
||||
.add_edge("worker", "reviewer") # Worker sends responses to Reviewer
|
||||
.add_edge("reviewer", "worker") # Reviewer provides feedback to Worker
|
||||
.set_start_executor("worker")
|
||||
.build()
|
||||
.as_agent() # Wrap workflow as an agent
|
||||
)
|
||||
|
||||
print("Running workflow agent with user query...")
|
||||
print("Query: 'Write code for parallel reading 1 million files on disk and write to a sorted output file.'")
|
||||
print("-" * 50)
|
||||
|
||||
# Run agent in streaming mode to observe incremental updates.
|
||||
async for event in agent.run_stream(
|
||||
"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!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Initializing Workflow as Agent Sample...")
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,167 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentThread, ChatAgent, ChatMessageStore, SequentialBuilder
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with Thread Conversation History and Checkpointing
|
||||
|
||||
This sample demonstrates how to use AgentThread with a workflow wrapped as an agent
|
||||
to maintain conversation history across multiple invocations. When using as_agent(),
|
||||
the thread's message store history is included in each workflow run, enabling
|
||||
the workflow participants to reference prior conversation context.
|
||||
|
||||
It also demonstrates how to enable checkpointing for workflow execution state
|
||||
persistence, allowing workflows to be paused and resumed.
|
||||
|
||||
Key concepts:
|
||||
- Workflows can be wrapped as agents using workflow.as_agent()
|
||||
- AgentThread with ChatMessageStore preserves conversation history
|
||||
- Each call to agent.run() includes thread history + new message
|
||||
- Participants in the workflow see the full conversation context
|
||||
- checkpoint_storage parameter enables workflow state persistence
|
||||
|
||||
Use cases:
|
||||
- Multi-turn conversations with workflow-based orchestrations
|
||||
- Stateful workflows that need context from previous interactions
|
||||
- Building conversational agents that leverage workflow patterns
|
||||
- Long-running workflows that need pause/resume capability
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI environment variables configured for OpenAIChatClient
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Create a chat client
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
# Define factory functions for workflow participants
|
||||
def create_assistant() -> ChatAgent:
|
||||
return chat_client.as_agent(
|
||||
name="assistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Answer questions based on the conversation "
|
||||
"history. If the user asks about something mentioned earlier, reference it."
|
||||
),
|
||||
)
|
||||
|
||||
def create_summarizer() -> ChatAgent:
|
||||
return chat_client.as_agent(
|
||||
name="summarizer",
|
||||
instructions=(
|
||||
"You are a summarizer. After the assistant responds, provide a brief "
|
||||
"one-sentence summary of the key point from the conversation so far."
|
||||
),
|
||||
)
|
||||
|
||||
# Build a sequential workflow: assistant -> summarizer
|
||||
workflow = SequentialBuilder().register_participants([create_assistant, create_summarizer]).build()
|
||||
|
||||
# Wrap the workflow as an agent
|
||||
agent = workflow.as_agent(name="ConversationalWorkflowAgent")
|
||||
|
||||
# Create a thread with a ChatMessageStore to maintain history
|
||||
message_store = ChatMessageStore()
|
||||
thread = AgentThread(message_store=message_store)
|
||||
|
||||
print("=" * 60)
|
||||
print("Workflow as Agent with Thread - Multi-turn Conversation")
|
||||
print("=" * 60)
|
||||
|
||||
# First turn: Introduce a topic
|
||||
query1 = "My name is Alex and I'm learning about machine learning."
|
||||
print(f"\n[Turn 1] User: {query1}")
|
||||
|
||||
response1 = await agent.run(query1, thread=thread)
|
||||
if response1.messages:
|
||||
for msg in response1.messages:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
# Second turn: Reference the previous topic
|
||||
query2 = "What was my name again, and what am I learning about?"
|
||||
print(f"\n[Turn 2] User: {query2}")
|
||||
|
||||
response2 = await agent.run(query2, thread=thread)
|
||||
if response2.messages:
|
||||
for msg in response2.messages:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
# Third turn: Ask a follow-up question
|
||||
query3 = "Can you suggest a good first project for me to try?"
|
||||
print(f"\n[Turn 3] User: {query3}")
|
||||
|
||||
response3 = await agent.run(query3, thread=thread)
|
||||
if response3.messages:
|
||||
for msg in response3.messages:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
# Show the accumulated conversation history
|
||||
print("\n" + "=" * 60)
|
||||
print("Full Thread History")
|
||||
print("=" * 60)
|
||||
if thread.message_store:
|
||||
history = await thread.message_store.list_messages()
|
||||
for i, msg in enumerate(history, start=1):
|
||||
role = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
|
||||
speaker = msg.author_name or role
|
||||
text_preview = msg.text[:80] + "..." if len(msg.text) > 80 else msg.text
|
||||
print(f"{i:02d}. [{speaker}]: {text_preview}")
|
||||
|
||||
|
||||
async def demonstrate_thread_serialization() -> None:
|
||||
"""
|
||||
Demonstrates serializing and resuming a thread with a workflow agent.
|
||||
|
||||
This shows how conversation history can be persisted and restored,
|
||||
enabling long-running conversational workflows.
|
||||
"""
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
def create_assistant() -> ChatAgent:
|
||||
return chat_client.as_agent(
|
||||
name="memory_assistant",
|
||||
instructions="You are a helpful assistant with good memory. Remember details from our conversation.",
|
||||
)
|
||||
|
||||
workflow = SequentialBuilder().register_participants([create_assistant]).build()
|
||||
agent = workflow.as_agent(name="MemoryWorkflowAgent")
|
||||
|
||||
# Create initial thread and have a conversation
|
||||
thread = AgentThread(message_store=ChatMessageStore())
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Thread Serialization Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# First interaction
|
||||
query = "Remember this: the secret code is ALPHA-7."
|
||||
print(f"\n[Session 1] User: {query}")
|
||||
response = await agent.run(query, thread=thread)
|
||||
if response.messages:
|
||||
print(f"[assistant]: {response.messages[0].text}")
|
||||
|
||||
# Serialize thread state (could be saved to database/file)
|
||||
serialized_state = await thread.serialize()
|
||||
print("\n[Serialized thread state for persistence]")
|
||||
|
||||
# Simulate a new session by creating a new thread from serialized state
|
||||
restored_thread = AgentThread(message_store=ChatMessageStore())
|
||||
await restored_thread.update_from_thread_state(serialized_state)
|
||||
|
||||
# Continue conversation with restored thread
|
||||
query = "What was the secret code I told you?"
|
||||
print(f"\n[Session 2 - Restored] User: {query}")
|
||||
response = await agent.run(query, thread=restored_thread)
|
||||
if response.messages:
|
||||
print(f"[assistant]: {response.messages[0].text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(demonstrate_thread_serialization())
|
||||
Reference in New Issue
Block a user