chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent, AgentResponseUpdate, WorkflowBuilder
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Sample: Azure AI Agents in a Workflow with Streaming
|
||||
|
||||
This sample shows how to create agents backed by Azure OpenAI Responses and use them in a workflow with streaming.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be the deployment name of a model in your Foundry project.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Create two agents: a Writer and a Reviewer.
|
||||
writer_agent = Agent(
|
||||
client=client,
|
||||
name="Writer",
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
)
|
||||
|
||||
reviewer_agent = Agent(
|
||||
client=client,
|
||||
name="Reviewer",
|
||||
instructions=(
|
||||
"You are an excellent content reviewer. "
|
||||
"Provide actionable feedback to the writer about the provided content. "
|
||||
"Provide the feedback in the most concise manner possible."
|
||||
),
|
||||
)
|
||||
|
||||
# Build the workflow by adding agents directly as edges.
|
||||
# Agents adapt to workflow mode: run(stream=True) for incremental updates, run() for complete responses.
|
||||
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
|
||||
|
||||
# Track the last author to format streaming output.
|
||||
last_author: str | None = None
|
||||
|
||||
events = workflow.run("Create a slogan for a new electric SUV that is affordable and fun to drive.", stream=True)
|
||||
async for event in events:
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
if last_author is not None:
|
||||
print() # Newline between different authors
|
||||
print(f"{author}: {update.text}", end="", flush=True)
|
||||
last_author = author
|
||||
else:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,113 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentExecutor,
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
InMemoryHistoryProvider,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowRunState,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Sample: Agents with a shared thread in a workflow
|
||||
|
||||
A Writer agent generates content, then a Reviewer agent critiques it, sharing a common message thread.
|
||||
|
||||
Purpose:
|
||||
Show how to use a shared thread between multiple agents in a workflow.
|
||||
By default, agents have individual threads, but sharing a thread allows them to share all messages.
|
||||
|
||||
Notes:
|
||||
- Not all agents can share threads; usually only the same type of agents can share threads.
|
||||
|
||||
Demonstrate:
|
||||
- Creating multiple agents with FoundryChatClient.
|
||||
- Setting up a shared thread between agents.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with agents, workflows, and executors in the agent framework.
|
||||
"""
|
||||
|
||||
|
||||
@executor(id="intercept_agent_response")
|
||||
async def intercept_agent_response(
|
||||
agent_response: AgentExecutorResponse, ctx: WorkflowContext[AgentExecutorRequest]
|
||||
) -> None:
|
||||
"""This executor intercepts the agent response and sends a request without messages.
|
||||
|
||||
This essentially prevents duplication of messages in the shared thread. Without this
|
||||
executor, the response will be added to the thread as input of the next agent call.
|
||||
"""
|
||||
await ctx.send_message(AgentExecutorRequest(messages=[]))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# set the same context provider (same default source_id) for both agents to share the thread
|
||||
writer = Agent(
|
||||
client=client,
|
||||
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
|
||||
name="writer",
|
||||
context_providers=[InMemoryHistoryProvider()],
|
||||
)
|
||||
|
||||
reviewer = Agent(
|
||||
client=client,
|
||||
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
|
||||
name="reviewer",
|
||||
context_providers=[InMemoryHistoryProvider()],
|
||||
)
|
||||
|
||||
# Create the shared session
|
||||
shared_session = writer.create_session()
|
||||
writer_executor = AgentExecutor(writer, session=shared_session)
|
||||
reviewer_executor = AgentExecutor(reviewer, session=shared_session)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder(start_executor=writer_executor)
|
||||
.add_chain([writer_executor, intercept_agent_response, reviewer_executor])
|
||||
.build()
|
||||
)
|
||||
|
||||
result = await workflow.run(
|
||||
"Write a tagline for a budget-friendly eBike.",
|
||||
# client_kwargs are forwarded to each underlying chat client call.
|
||||
# store=False tells the model API not to persist messages server-side
|
||||
# for this example.
|
||||
client_kwargs={"store": False},
|
||||
)
|
||||
|
||||
# The final state should be IDLE since the workflow no longer has messages to
|
||||
# process after the reviewer agent responds.
|
||||
assert result.get_final_state() == WorkflowRunState.IDLE
|
||||
|
||||
# The shared session now contains the conversation between the writer and reviewer. Print it out.
|
||||
print("=== Shared Session Conversation ===")
|
||||
memory_state = shared_session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {})
|
||||
for message in memory_state.get("messages", []):
|
||||
print(f"{message.author_name or message.role}: {message.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,159 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Final
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
AgentResponseUpdate,
|
||||
Message,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Sample: AzureOpenAI Chat Agents and an Executor in a Workflow with Streaming
|
||||
|
||||
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.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- 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.
|
||||
|
||||
Args:
|
||||
draft: The response from the research_agent containing the initial draft. This is
|
||||
a `AgentExecutorResponse` because agents in workflows send their full response
|
||||
wrapped in this type to connected executors.
|
||||
ctx: The workflow context to send the next request.
|
||||
"""
|
||||
conversation = list(draft.full_conversation or draft.agent_response.messages)
|
||||
original_prompt = next((message.text for message in conversation if message.role == "user"), "")
|
||||
external_note = _lookup_external_note(original_prompt) or (
|
||||
"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(Message("user", [follow_up]))
|
||||
|
||||
# Output a new AgentExecutorRequest for the next agent in the workflow.
|
||||
# Agents in workflows handle this type and will generate a response based on the request.
|
||||
await ctx.send_message(AgentExecutorRequest(messages=conversation))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the workflow and stream combined updates from both agents."""
|
||||
# Create the agents
|
||||
research_agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
name="research_agent",
|
||||
instructions=(
|
||||
"Produce a short, bullet-style briefing with two actionable ideas. Label the section as 'Initial Draft'."
|
||||
),
|
||||
)
|
||||
|
||||
final_editor_agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
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."
|
||||
),
|
||||
)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder(start_executor=research_agent)
|
||||
.add_edge(research_agent, enrich_with_references)
|
||||
.add_edge(enrich_with_references, final_editor_agent)
|
||||
.build()
|
||||
)
|
||||
|
||||
events = workflow.run(
|
||||
"Create quick workspace wellness tips for a remote analyst working across two monitors.", stream=True
|
||||
)
|
||||
|
||||
# Track the last author to format streaming output.
|
||||
last_author: str | None = None
|
||||
|
||||
async for event in events:
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
if last_author is not None:
|
||||
print("\n") # Newline between different authors
|
||||
print(f"{author}: {update.text}", end="", flush=True)
|
||||
last_author = author
|
||||
else:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,83 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent, AgentResponseUpdate, WorkflowBuilder
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Sample: AzureOpenAI Chat Agents in a Workflow with Streaming
|
||||
|
||||
This sample shows how to create AzureOpenAI Chat Agents and use them in a workflow with streaming.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
async def main():
|
||||
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
|
||||
# Create the agents
|
||||
_writer_client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
writer_agent = Agent(
|
||||
client=_writer_client,
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
name="writer",
|
||||
)
|
||||
|
||||
_reviewer_client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
reviewer_agent = Agent(
|
||||
client=_reviewer_client,
|
||||
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",
|
||||
)
|
||||
|
||||
# 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=True) for incremental updates, run() for complete responses.
|
||||
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
|
||||
|
||||
# Track the last author to format streaming output.
|
||||
last_author: str | None = None
|
||||
|
||||
events = workflow.run("Create a slogan for a new electric SUV that is affordable and fun to drive.", stream=True)
|
||||
async for event in events:
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
if last_author is not None:
|
||||
print() # Newline between different authors
|
||||
print(f"{author}: {update.text}", end="", flush=True)
|
||||
last_author = author
|
||||
else:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,323 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from collections.abc import AsyncIterable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentExecutor,
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
AgentResponse,
|
||||
Executor,
|
||||
Message,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field
|
||||
from typing_extensions import Never
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
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 request_info event (type='request_info') 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:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- Authentication via azure-identity. Run `az login` before executing.
|
||||
"""
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
|
||||
# see samples/02-agents/tools/function_tool_with_approval.py and
|
||||
# samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def fetch_product_brief(
|
||||
product_name: Annotated[str, Field(description="Product name to look up.")],
|
||||
) -> str:
|
||||
"""Return a marketing brief for a product."""
|
||||
briefs = {
|
||||
"lumenx desk lamp": (
|
||||
"Product: LumenX Desk Lamp\n"
|
||||
"- Three-point adjustable arm with 270° rotation.\n"
|
||||
"- Custom warm-to-neutral LED spectrum (2700K-4000K).\n"
|
||||
"- USB-C charging pad integrated in the base.\n"
|
||||
"- Designed for home offices and late-night study sessions."
|
||||
)
|
||||
}
|
||||
return briefs.get(product_name.lower(), f"No stored brief for '{product_name}'.")
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_brand_voice_profile(
|
||||
voice_name: Annotated[str, Field(description="Brand or campaign voice to emulate.")],
|
||||
) -> str:
|
||||
"""Return guidance for the requested brand voice."""
|
||||
voices = {
|
||||
"lumenx launch": (
|
||||
"Voice guidelines:\n"
|
||||
"- Friendly and modern with concise sentences.\n"
|
||||
"- Highlight practical benefits before aesthetics.\n"
|
||||
"- End with an invitation to imagine the product in daily use."
|
||||
)
|
||||
}
|
||||
return voices.get(voice_name.lower(), f"No stored voice profile for '{voice_name}'.")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DraftFeedbackRequest:
|
||||
"""Payload sent for human review."""
|
||||
|
||||
prompt: str = ""
|
||||
draft_text: str = ""
|
||||
conversation: list[Message] = 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(draft.full_conversation)
|
||||
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,
|
||||
*[Message("user", contents=["The draft is approved as-is."])],
|
||||
],
|
||||
should_respond=True,
|
||||
),
|
||||
target_id=self.final_editor_id,
|
||||
)
|
||||
return
|
||||
|
||||
# Human provided feedback; prompt the writer to revise.
|
||||
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."
|
||||
)
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=[Message("user", contents=[instruction])], should_respond=True),
|
||||
target_id=self.writer_id,
|
||||
)
|
||||
|
||||
|
||||
def create_writer_agent() -> Agent:
|
||||
"""Creates a writer agent with tools."""
|
||||
return Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
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],
|
||||
default_options={
|
||||
"tool_choice": "required",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def create_final_editor_agent() -> Agent:
|
||||
"""Creates a final editor agent."""
|
||||
return Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
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: WorkflowEvent, 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 c.type == "function_call"] # type: ignore[union-attr]
|
||||
function_results = [c for c in update.contents if c.type == "function_result"] # 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 consume_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, str] | None:
|
||||
"""Consume a workflow event stream, printing outputs and returning any pending human responses."""
|
||||
requests: list[WorkflowEvent] = []
|
||||
async for event in stream:
|
||||
if event.type == "request_info" and isinstance(event.data, DraftFeedbackRequest):
|
||||
# Stash the request so we can prompt the human after the stream completes.
|
||||
requests.append(event)
|
||||
|
||||
if requests:
|
||||
pending_responses: dict[str, str] = {}
|
||||
for request in requests:
|
||||
print("\n----- Writer draft -----")
|
||||
print(request.data.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...")
|
||||
exit(0)
|
||||
pending_responses[request.request_id] = answer
|
||||
|
||||
return pending_responses
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the workflow and bridge human feedback between two agents."""
|
||||
|
||||
# Build the workflow.
|
||||
writer_agent = AgentExecutor(create_writer_agent())
|
||||
final_editor_agent = AgentExecutor(create_final_editor_agent())
|
||||
coordinator = Coordinator(
|
||||
id="coordinator",
|
||||
writer_id="writer_agent",
|
||||
final_editor_id="final_editor_agent",
|
||||
)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder(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()
|
||||
)
|
||||
|
||||
print(
|
||||
"Interactive mode. When prompted, provide a short feedback note for the editor.",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run.
|
||||
stream = workflow.run(
|
||||
"Create a short launch blurb for the LumenX desk lamp. Emphasize adjustability and warm lighting.",
|
||||
stream=True,
|
||||
)
|
||||
pending_responses = await consume_stream(stream)
|
||||
|
||||
# Run until there are no more requests
|
||||
while pending_responses is not None:
|
||||
stream = workflow.run(stream=True, responses=pending_responses)
|
||||
pending_responses = await consume_stream(stream)
|
||||
|
||||
print("Workflow complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import ConcurrentBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
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 `Agent(client=workflow,...)` 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 `Agent(client=workflow,...)`.
|
||||
- Workflow completion when idle with no pending work
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- Familiarity with Workflow events (WorkflowEvent with type "output")
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 1) Create three domain agents using FoundryChatClient
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
researcher = Agent(
|
||||
client=client,
|
||||
instructions=(
|
||||
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
|
||||
" opportunities, and risks."
|
||||
),
|
||||
name="researcher",
|
||||
)
|
||||
|
||||
marketer = Agent(
|
||||
client=client,
|
||||
instructions=(
|
||||
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
|
||||
" aligned to the prompt."
|
||||
),
|
||||
name="marketer",
|
||||
)
|
||||
|
||||
legal = Agent(
|
||||
client=client,
|
||||
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()
|
||||
prompt = "We are launching a new budget-friendly electric bike for urban commuters."
|
||||
|
||||
agent_response = await agent.run(prompt)
|
||||
print("===== Final Aggregated Response =====\n")
|
||||
for message in agent_response.messages:
|
||||
# The agent_response contains messages from all participants concatenated
|
||||
# into a single message.
|
||||
print(f"{message.author_name}: {message.text}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,146 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
Executor,
|
||||
Message,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Sample: Custom Agent Executors in a Workflow
|
||||
|
||||
This sample uses two custom executors. A Writer agent creates or edits content,
|
||||
then hands the conversation to a Reviewer agent which evaluates and finalizes the result.
|
||||
|
||||
Purpose:
|
||||
Show how to wrap chat agents created by FoundryChatClient inside workflow executors. Demonstrate the @handler
|
||||
pattern with typed inputs and typed WorkflowContext[T] outputs, connect executors with the fluent WorkflowBuilder,
|
||||
and finish by yielding outputs from the terminal node.
|
||||
|
||||
Note: When an agent is passed to a workflow, the workflow wraps the agent in a more sophisticated executor.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- 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 Agent 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: Agent
|
||||
|
||||
def __init__(self, id: str = "writer"):
|
||||
# Create a domain specific agent using your configured FoundryChatClient.
|
||||
self.agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
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: Message, ctx: WorkflowContext[list[Message], str]) -> None:
|
||||
"""Generate content using the agent and forward the updated conversation.
|
||||
|
||||
Contract for this handler:
|
||||
- message is the inbound user Message.
|
||||
- ctx is a WorkflowContext that expects a list[Message] 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[Message] = [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: Agent
|
||||
|
||||
def __init__(self, id: str = "reviewer"):
|
||||
# Create a domain specific agent that evaluates and refines content.
|
||||
self.agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
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[Message], ctx: WorkflowContext[list[Message], 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."""
|
||||
# Create the executors
|
||||
writer = Writer()
|
||||
reviewer = Reviewer()
|
||||
|
||||
# Build the workflow using the fluent builder.
|
||||
# Set the start node and connect an edge from writer to reviewer.
|
||||
workflow = WorkflowBuilder(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(
|
||||
Message("user", ["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,92 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import GroupChatBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
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:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- Environment variables configured for `FoundryChatClient`.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
description="Collects relevant background information.",
|
||||
instructions="Gather concise facts that help a teammate answer the question.",
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
description="Synthesizes a polished answer using the gathered notes.",
|
||||
instructions="Compose clear and structured answers using any notes provided.",
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
)
|
||||
|
||||
_orch_client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Mark participant responses as intermediate so workflow.as_agent() maps
|
||||
# them to text_reasoning content while the final answer remains normal text.
|
||||
workflow = GroupChatBuilder(
|
||||
participants=[researcher, writer],
|
||||
intermediate_output_from=[researcher, writer],
|
||||
orchestrator_agent=Agent(
|
||||
client=_orch_client,
|
||||
name="Orchestrator",
|
||||
instructions="You coordinate a team conversation to solve the user's task.",
|
||||
),
|
||||
).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()
|
||||
agent_result = await workflow_agent.run(task)
|
||||
|
||||
if agent_result.messages:
|
||||
# The output should contain a message from the researcher, a message from the writer,
|
||||
# and a final synthesized answer from the orchestrator.
|
||||
print("\n===== as_agent() Transcript =====")
|
||||
for i, msg in enumerate(agent_result.messages, start=1):
|
||||
role_value = getattr(msg.role, "value", msg.role)
|
||||
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,235 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentResponse,
|
||||
Content,
|
||||
Message,
|
||||
WorkflowAgent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""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:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- `az login` (Azure CLI authentication)
|
||||
- Environment variables configured for FoundryChatClient (FOUNDRY_MODEL)
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
|
||||
# See:
|
||||
# samples/02-agents/tools/function_tool_with_approval.py
|
||||
# samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
|
||||
"""Simulated function to process a refund for a given order number."""
|
||||
return f"Refund processed successfully for order {order_number}."
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
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."
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
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(client: FoundryChatClient) -> tuple[Agent, Agent, Agent, Agent]:
|
||||
"""Create and configure the triage and specialist agents.
|
||||
|
||||
Args:
|
||||
client: The FoundryChatClient 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 = Agent(
|
||||
client=client,
|
||||
instructions=(
|
||||
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
|
||||
"based on the problem described."
|
||||
),
|
||||
name="triage_agent",
|
||||
require_per_service_call_history_persistence=True,
|
||||
)
|
||||
|
||||
# Refund specialist: Handles refund requests
|
||||
refund_agent = Agent(
|
||||
client=client,
|
||||
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],
|
||||
require_per_service_call_history_persistence=True,
|
||||
)
|
||||
|
||||
# Order/shipping specialist: Resolves delivery issues
|
||||
order_agent = Agent(
|
||||
client=client,
|
||||
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],
|
||||
require_per_service_call_history_persistence=True,
|
||||
)
|
||||
|
||||
# Return specialist: Handles return requests
|
||||
return_agent = Agent(
|
||||
client=client,
|
||||
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],
|
||||
require_per_service_call_history_persistence=True,
|
||||
)
|
||||
|
||||
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}: {message.text}")
|
||||
for content in message.contents:
|
||||
if content.type == "function_call" and content.name == WorkflowAgent.REQUEST_INFO_FUNCTION_NAME:
|
||||
request_function_args = WorkflowAgent.RequestInfoFunctionArgs.from_dict(content.arguments) # type: ignore
|
||||
request_id = request_function_args.request_id
|
||||
request_event = request_function_args.request_event
|
||||
pending_requests[request_id] = request_event.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
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Create all agents: triage + specialists
|
||||
triage, refund, order, support = create_agents(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
|
||||
# - 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],
|
||||
# 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.
|
||||
termination_condition=lambda conversation: (
|
||||
len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
|
||||
),
|
||||
)
|
||||
.with_start_agent(triage)
|
||||
.build()
|
||||
.as_agent()
|
||||
)
|
||||
|
||||
# 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:
|
||||
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 = [
|
||||
Content("function_result", call_id=req_id, result=response) for req_id, response in responses.items()
|
||||
]
|
||||
response = await agent.run(Message("tool", function_results))
|
||||
pending_requests = handle_response_and_requests(response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,119 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import MagenticBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
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 `Agent(client=workflow, ...)` so the entire Magentic loop can be reused
|
||||
like any other agent while still emitting callback telemetry.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
researcher_agent = Agent(
|
||||
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.
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
)
|
||||
|
||||
# Create code interpreter tool using instance method
|
||||
coder_client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
code_interpreter_tool = coder_client.get_code_interpreter_tool()
|
||||
|
||||
coder_agent = Agent(
|
||||
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.",
|
||||
client=coder_client,
|
||||
tools=code_interpreter_tool,
|
||||
)
|
||||
|
||||
# Create a manager agent for orchestration
|
||||
manager_agent = Agent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the research and coding workflow",
|
||||
instructions="You coordinate a team to complete complex tasks efficiently.",
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
)
|
||||
|
||||
print("\nBuilding Magentic Workflow...")
|
||||
|
||||
# Mark participant responses as intermediate so workflow.as_agent() maps
|
||||
# them to text_reasoning content while the final answer remains normal text.
|
||||
workflow = MagenticBuilder(
|
||||
participants=[researcher_agent, coder_agent],
|
||||
intermediate_output_from=[researcher_agent, coder_agent],
|
||||
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()
|
||||
|
||||
last_response_id: str | None = None
|
||||
async for update in workflow_agent.run(task, stream=True):
|
||||
# Fallback for any other events with text
|
||||
if last_response_id != update.response_id:
|
||||
if last_response_id is not None:
|
||||
print() # Newline between different responses
|
||||
print(f"{update.author_name}: ", end="", flush=True)
|
||||
last_response_id = update.response_id
|
||||
else:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Workflow execution failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
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 `Agent(client=workflow,...)` 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:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be set to the Azure Foundry project endpoint.
|
||||
- FOUNDRY_MODEL must be set to the model name for the Foundry chat client.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 1) Create agents
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
client=client,
|
||||
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
|
||||
name="writer",
|
||||
)
|
||||
|
||||
reviewer = Agent(
|
||||
client=client,
|
||||
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()
|
||||
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):
|
||||
name = msg.author_name or msg.role
|
||||
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
===== Conversation =====
|
||||
------------------------------------------------------------
|
||||
01 [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.
|
||||
|
||||
Note:
|
||||
`workflow.as_agent()` returns ONLY the final agent's response (the "answer") — the prior agents' work
|
||||
is not included in the response. To preserve earlier participant replies while running as an agent, build with
|
||||
`SequentialBuilder(participants=[...], intermediate_output_from=[writer])`; intermediate workflow events become
|
||||
`text_reasoning` content on the AgentResponse, while `.text` remains terminal-output only.
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,166 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Ensure local 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))
|
||||
# Also add the current directory for sibling imports
|
||||
_CURRENT_DIR = str(Path(__file__).resolve().parent)
|
||||
if _CURRENT_DIR not in sys.path:
|
||||
sys.path.insert(0, _CURRENT_DIR)
|
||||
|
||||
from agent_framework import ( # noqa: E402
|
||||
Content,
|
||||
Executor,
|
||||
Message,
|
||||
WorkflowAgent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
from workflow_as_agent_reflection_pattern import ( # pyrefly: ignore[missing-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:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- 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.
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@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.
|
||||
worker = Worker(
|
||||
id="worker",
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
)
|
||||
reviewer = ReviewerWithHumanInTheLoop(worker_id="worker")
|
||||
|
||||
agent = (
|
||||
WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build().as_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 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: Content | None = None
|
||||
for message in response.messages:
|
||||
for content in message.contents:
|
||||
if 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 = WorkflowAgent.RequestInfoFunctionArgs.from_dict(human_review_function_call.arguments) # type: ignore
|
||||
request_payload = human_request_args.request_event.data
|
||||
if not isinstance(request_payload, HumanReviewRequest):
|
||||
raise ValueError("Human review request payload must be a HumanReviewRequest.")
|
||||
if not request_payload.agent_request:
|
||||
raise ValueError("Human review request must contain an agent_request.")
|
||||
# Mock a human response approval for demonstration purposes.
|
||||
human_response = ReviewResponse(request_id=request_payload.agent_request.request_id, feedback="", approved=True)
|
||||
# Create the function call result object to send back to the agent.
|
||||
human_review_function_result = Content(
|
||||
"function_result",
|
||||
call_id=human_review_function_call.call_id, # type: ignore
|
||||
result=human_response,
|
||||
)
|
||||
# Send the human review result back to the agent.
|
||||
response = await agent.run(Message("tool", [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,156 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import Annotated, Any
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with kwargs Propagation to @tool Tools
|
||||
|
||||
This sample demonstrates how to flow custom context (skill data, user tokens, etc.)
|
||||
through a workflow exposed Agent(client=via,) to @tool functions using the **kwargs pattern.
|
||||
|
||||
Key Concepts:
|
||||
- Build a workflow using SequentialBuilder (or any builder pattern)
|
||||
- Expose the workflow as a reusable agent via Agent(client=workflow,)
|
||||
- Pass custom context as kwargs when invoking workflow_agent.run()
|
||||
- kwargs are stored in State and propagated to all agent invocations
|
||||
- @tool functions receive kwargs via **kwargs parameter
|
||||
|
||||
When to use Agent(client=workflow,):
|
||||
- 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:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
"""
|
||||
|
||||
|
||||
# Define tools that accept custom context via **kwargs
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and
|
||||
# samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
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}"
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
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
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Create agent with tools that use kwargs
|
||||
agent = Agent(
|
||||
client=client,
|
||||
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 Agent(client=using,)
|
||||
workflow_agent = workflow.as_agent()
|
||||
|
||||
# Define custom context that will flow to tools 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 tools
|
||||
# Note: kwargs are passed to workflow.run()
|
||||
print("\n===== Streaming Response =====")
|
||||
async for update in workflow_agent.run(
|
||||
"Please get my user data and then call the users API endpoint.",
|
||||
function_invocation_kwargs={"custom_data": custom_data, "user_token": user_token},
|
||||
stream=True,
|
||||
):
|
||||
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,235 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
Executor,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
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.
|
||||
- Structured output parsing for review feedback using Pydantic.
|
||||
- State management for pending requests and retry logic.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
- 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[Message]
|
||||
agent_messages: list[Message]
|
||||
|
||||
|
||||
@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, client: SupportsChatGetResponse) -> None:
|
||||
super().__init__(id=id)
|
||||
self._chat_client = 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 = [
|
||||
Message(
|
||||
role="system",
|
||||
contents=[
|
||||
(
|
||||
"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(Message("user", ["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, client: SupportsChatGetResponse) -> None:
|
||||
super().__init__(id=id)
|
||||
self._chat_client = client
|
||||
self._pending_requests: dict[str, tuple[ReviewRequest, list[Message]]] = {}
|
||||
|
||||
@handler
|
||||
async def handle_user_messages(self, user_messages: list[Message], ctx: WorkflowContext[ReviewRequest]) -> None:
|
||||
print("Worker: Received user messages, generating response...")
|
||||
|
||||
# Initialize chat with system prompt.
|
||||
messages = [Message("system", ["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, AgentResponse]
|
||||
) -> None:
|
||||
print(f"Worker: Received review for request {review.request_id[:8]} - Approved: {review.approved}")
|
||||
|
||||
if review.request_id not in self._pending_requests:
|
||||
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...")
|
||||
# Emit approved result to external consumer
|
||||
await ctx.yield_output(AgentResponse(messages=request.agent_messages))
|
||||
return
|
||||
|
||||
print(f"Worker: Response not approved. Feedback: {review.feedback}")
|
||||
print("Worker: Regenerating response with feedback...")
|
||||
|
||||
# Incorporate review feedback.
|
||||
messages.append(Message("system", [review.feedback]))
|
||||
messages.append(Message("system", ["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...")
|
||||
worker = Worker(
|
||||
id="worker",
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
)
|
||||
reviewer = Reviewer(
|
||||
id="reviewer",
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
),
|
||||
)
|
||||
|
||||
agent = (
|
||||
WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build().as_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.
|
||||
response = await agent.run(
|
||||
"Write code for parallel reading 1 million files on disk and write to a sorted output file."
|
||||
)
|
||||
|
||||
print("-" * 50)
|
||||
print("Final Approved Response:")
|
||||
print(f"{response.agent_id}: {response.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Initializing Workflow as Agent Sample...")
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,180 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent, AgentSession, InMemoryHistoryProvider
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with Session Conversation History and Checkpointing
|
||||
|
||||
This sample demonstrates how to use AgentSession with a workflow wrapped as an agent
|
||||
to maintain conversation history across multiple invocations. When using as_agent(),
|
||||
the session's 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 Agent(client=workflow,)
|
||||
- AgentSession preserves conversation history
|
||||
- Each call to agent.run() includes session 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:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Create a chat client
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
assistant = Agent(
|
||||
client=client,
|
||||
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."
|
||||
),
|
||||
)
|
||||
|
||||
summarizer = Agent(
|
||||
client=client,
|
||||
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(participants=[assistant, summarizer]).build()
|
||||
|
||||
# Wrap the workflow as an agent
|
||||
agent = workflow.as_agent()
|
||||
|
||||
# Create a session to maintain history
|
||||
session = agent.create_session()
|
||||
|
||||
print("=" * 60)
|
||||
print("Workflow as Agent with Session - 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, session=session)
|
||||
if response1.messages:
|
||||
for msg in response1.messages:
|
||||
speaker = msg.author_name or msg.role
|
||||
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, session=session)
|
||||
if response2.messages:
|
||||
for msg in response2.messages:
|
||||
speaker = msg.author_name or msg.role
|
||||
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, session=session)
|
||||
if response3.messages:
|
||||
for msg in response3.messages:
|
||||
speaker = msg.author_name or msg.role
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
# Show the accumulated conversation history
|
||||
print("\n" + "=" * 60)
|
||||
print("Full Session History")
|
||||
print("=" * 60)
|
||||
memory_state = session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {})
|
||||
history = memory_state.get("messages", [])
|
||||
for i, msg in enumerate(history, start=1):
|
||||
role = msg.role 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_session_serialization() -> None:
|
||||
"""
|
||||
Demonstrates serializing and resuming a session with a workflow agent.
|
||||
|
||||
This shows how conversation history can be persisted and restored,
|
||||
enabling long-running conversational workflows.
|
||||
"""
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
memory_assistant = Agent(
|
||||
client=client,
|
||||
name="memory_assistant",
|
||||
instructions="You are a helpful assistant with good memory. Remember details from our conversation.",
|
||||
)
|
||||
|
||||
workflow = SequentialBuilder(participants=[memory_assistant]).build()
|
||||
agent = workflow.as_agent()
|
||||
|
||||
# Create initial session and have a conversation
|
||||
session = agent.create_session()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Session 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, session=session)
|
||||
if response.messages:
|
||||
print(f"[assistant]: {response.messages[0].text}")
|
||||
|
||||
# Serialize session state (could be saved to database/file)
|
||||
serialized_state = session.to_dict()
|
||||
print("\n[Serialized session state for persistence]")
|
||||
|
||||
# Simulate a new session by creating a new session from serialized state
|
||||
restored_session = AgentSession.from_dict(serialized_state)
|
||||
|
||||
# Continue conversation with restored session
|
||||
query = "What was the secret code I told you?"
|
||||
print(f"\n[Session 2 - Restored] User: {query}")
|
||||
response = await agent.run(query, session=restored_session)
|
||||
if response.messages:
|
||||
print(f"[assistant]: {response.messages[0].text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(demonstrate_session_serialization())
|
||||
Reference in New Issue
Block a user