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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,37 @@
# Customer Support Workflow Sample
Multi-agent workflow demonstrating automated troubleshooting with escalation paths.
## Overview
Coordinates six specialized agents to handle customer support requests:
1. **SelfServiceAgent** - Initial troubleshooting with user
2. **TicketingAgent** - Creates tickets when escalation needed
3. **TicketRoutingAgent** - Routes to appropriate team
4. **WindowsSupportAgent** - Windows-specific troubleshooting
5. **TicketResolutionAgent** - Resolves tickets
6. **TicketEscalationAgent** - Escalates to human support
## Files
- `workflow.yaml` - Workflow definition with conditional routing
- `main.py` - Agent definitions and workflow execution
- `ticketing_plugin.py` - Mock ticketing system plugin
## Running
```bash
python main.py
```
## Example Input
```
My PC keeps rebooting and I can't use it.
```
## Requirements
- Azure OpenAI endpoint configured
- `az login` for authentication
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,361 @@
# Copyright (c) Microsoft. All rights reserved.
"""
CustomerSupport workflow sample.
This workflow demonstrates using multiple agents to provide automated
troubleshooting steps to resolve common issues with escalation options.
Example input: "My PC keeps rebooting and I can't use it."
Usage:
python main.py
The workflow:
1. SelfServiceAgent: Works with user to provide troubleshooting steps
2. TicketingAgent: Creates a ticket if issue needs escalation
3. TicketRoutingAgent: Determines which team should handle the ticket
4. WindowsSupportAgent: Provides Windows-specific troubleshooting
5. TicketResolutionAgent: Resolves the ticket when issue is fixed
6. TicketEscalationAgent: Escalates to human support if needed
"""
import asyncio
import json
import logging
import os
import uuid
from pathlib import Path
from typing import Any
from agent_framework import Agent
from agent_framework.declarative import (
AgentExternalInputRequest,
AgentExternalInputResponse,
WorkflowFactory,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.openai import OpenAIChatOptions
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from ticketing_plugin import TicketingPlugin # ty: ignore[unresolved-import] # pyrefly: ignore[missing-import]
logging.basicConfig(level=logging.ERROR)
# Load environment variables from .env file
load_dotenv()
# ANSI color codes for output formatting
CYAN = "\033[36m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
MAGENTA = "\033[35m"
RESET = "\033[0m"
# Agent Instructions
SELF_SERVICE_INSTRUCTIONS = """
Use your knowledge to work with the user to provide the best possible troubleshooting steps.
- If the user confirms that the issue is resolved, then the issue is resolved.
- If the user reports that the issue persists, then escalate.
""".strip()
TICKETING_INSTRUCTIONS = """Always create a ticket in Azure DevOps using the available tools.
Include the following information in the TicketSummary.
- Issue description: {{IssueDescription}}
- Attempted resolution steps: {{AttemptedResolutionSteps}}
After creating the ticket, provide the user with the ticket ID."""
TICKET_ROUTING_INSTRUCTIONS = """Determine how to route the given issue to the appropriate support team.
Choose from the available teams and their functions:
- Windows Activation Support: Windows license activation issues
- Windows Support: Windows related issues
- Azure Support: Azure related issues
- Network Support: Network related issues
- Hardware Support: Hardware related issues
- Microsoft Office Support: Microsoft Office related issues
- General Support: General issues not related to the above categories"""
WINDOWS_SUPPORT_INSTRUCTIONS = """
Use your knowledge to work with the user to provide the best possible troubleshooting steps
for issues related to Windows operating system.
- Utilize the "Attempted Resolutions Steps" as a starting point for your troubleshooting.
- Never escalate without troubleshooting with the user.
- If the user confirms that the issue is resolved, then the issue is resolved.
- If the user reports that the issue persists, then escalate.
Issue: {{IssueDescription}}
Attempted Resolution Steps: {{AttemptedResolutionSteps}}"""
RESOLUTION_INSTRUCTIONS = """Resolve the following ticket in Azure DevOps.
Always include the resolution details.
- Ticket ID: #{{TicketId}}
- Resolution Summary: {{ResolutionSummary}}"""
ESCALATION_INSTRUCTIONS = """
You escalate the provided issue to human support team by sending an email.
Here are some additional details that might help:
- TicketId : {{TicketId}}
- IssueDescription : {{IssueDescription}}
- AttemptedResolutionSteps : {{AttemptedResolutionSteps}}
Before escalating, gather the user's email address for follow-up.
If not known, ask the user for their email address so that the support team can reach them when needed.
When sending the email, include the following details:
- To: support@contoso.com
- Cc: user's email address
- Subject of the email: "Support Ticket - {TicketId} - [Compact Issue Description]"
- Body:
- Issue description
- Attempted resolution steps
- User's email address
- Any other relevant information from the conversation history
Assure the user that their issue will be resolved and provide them with a ticket ID for reference."""
# Pydantic models for structured outputs
class SelfServiceResponse(BaseModel):
"""Response from self-service agent evaluation."""
IsResolved: bool = Field(description="True if the user issue/ask has been resolved.")
NeedsTicket: bool = Field(description="True if the user issue/ask requires that a ticket be filed.")
IssueDescription: str = Field(description="A concise description of the issue.")
AttemptedResolutionSteps: str = Field(description="An outline of the steps taken to attempt resolution.")
class TicketingResponse(BaseModel):
"""Response from ticketing agent."""
TicketId: str = Field(description="The identifier of the ticket created in response to the user issue.")
TicketSummary: str = Field(description="The summary of the ticket created in response to the user issue.")
class RoutingResponse(BaseModel):
"""Response from routing agent."""
TeamName: str = Field(description="The name of the team to route the issue")
class SupportResponse(BaseModel):
"""Response from support agent."""
IsResolved: bool = Field(description="True if the user issue/ask has been resolved.")
NeedsEscalation: bool = Field(
description="True resolution could not be achieved and the issue/ask requires escalation."
)
ResolutionSummary: str = Field(description="The summary of the steps that led to resolution.")
class EscalationResponse(BaseModel):
"""Response from escalation agent."""
IsComplete: bool = Field(description="Has the email been sent and no more user input is required.")
UserMessage: str = Field(description="A natural language message to the user.")
async def main() -> None:
"""Run the customer support workflow."""
# Create ticketing plugin
plugin = TicketingPlugin()
# Create Azure OpenAI client
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
# This sample has been tested only on `gpt-5.1` and may not work as intended on other models
# This sample is known to fail on `gpt-5-mini` reasoning input (GH issue #4059)
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Create agents with structured outputs
self_service_agent = Agent(
client=client,
name="SelfServiceAgent",
instructions=SELF_SERVICE_INSTRUCTIONS,
default_options=OpenAIChatOptions[Any](response_format=SelfServiceResponse),
)
ticketing_agent = Agent(
client=client,
name="TicketingAgent",
instructions=TICKETING_INSTRUCTIONS,
tools=plugin.get_functions(),
default_options=OpenAIChatOptions[Any](response_format=TicketingResponse),
)
routing_agent = Agent(
client=client,
name="TicketRoutingAgent",
instructions=TICKET_ROUTING_INSTRUCTIONS,
tools=[plugin.get_ticket],
default_options=OpenAIChatOptions[Any](response_format=RoutingResponse),
)
windows_support_agent = Agent(
client=client,
name="WindowsSupportAgent",
instructions=WINDOWS_SUPPORT_INSTRUCTIONS,
tools=[plugin.get_ticket],
default_options=OpenAIChatOptions[Any](response_format=SupportResponse),
)
resolution_agent = Agent(
client=client,
name="TicketResolutionAgent",
instructions=RESOLUTION_INSTRUCTIONS,
tools=[plugin.resolve_ticket],
)
escalation_agent = Agent(
client=client,
name="TicketEscalationAgent",
instructions=ESCALATION_INSTRUCTIONS,
tools=[plugin.get_ticket, plugin.send_notification],
default_options=OpenAIChatOptions[Any](response_format=EscalationResponse),
)
# Agent registry for lookup
agents = {
"SelfServiceAgent": self_service_agent,
"TicketingAgent": ticketing_agent,
"TicketRoutingAgent": routing_agent,
"WindowsSupportAgent": windows_support_agent,
"TicketResolutionAgent": resolution_agent,
"TicketEscalationAgent": escalation_agent,
}
# Print loaded agents (similar to .NET "PROMPT AGENT: AgentName:1")
for agent_name in agents:
print(f"{CYAN}PROMPT AGENT: {agent_name}:1{RESET}")
# Create workflow factory
factory = WorkflowFactory(agents=agents)
# Load workflow from YAML
samples_root = Path(__file__).parent.parent.parent.parent.parent.parent.parent
workflow_path = samples_root / "declarative-agents" / "workflow-samples" / "CustomerSupport.yaml"
if not workflow_path.exists():
# Fall back to local copy if declarative-agents/workflow-samples doesn't exist
workflow_path = Path(__file__).parent / "workflow.yaml"
workflow = factory.create_workflow_from_yaml_path(workflow_path)
print()
print("=" * 60)
# Example input
user_input = "My computer won't boot"
pending_request_id: str | None = None
# Track responses for formatting
accumulated_response: str = ""
last_agent_name: str | None = None
print(f"\n{GREEN}INPUT:{RESET} {user_input}\n")
while True:
if pending_request_id:
# Continue workflow with user response
print(f"\n{YELLOW}WORKFLOW:{RESET} Restore\n")
response = AgentExternalInputResponse(user_input=user_input)
stream = workflow.run(stream=True, responses={pending_request_id: response})
pending_request_id = None
else:
# Start workflow
stream = workflow.run(user_input, stream=True)
async for event in stream:
if event.type == "output":
data = event.data
# source_executor_id is only available on request_info events.
# For output events, use executor_id to identify the emitting node.
source_id = event.executor_id or ""
# Check if this is a SendActivity output (activity text from log_ticket, log_route, etc.)
if "log_" in source_id.lower():
# Print any accumulated agent response first
if accumulated_response and last_agent_name:
msg_id = f"msg_{uuid.uuid4().hex[:32]}"
print(f"{CYAN}{last_agent_name.upper()}:{RESET} [{msg_id}]")
try:
parsed = json.loads(accumulated_response)
print(json.dumps(parsed))
except (json.JSONDecodeError, TypeError):
print(accumulated_response)
accumulated_response = ""
last_agent_name = None
# Print activity
print(f"\n{MAGENTA}ACTIVITY:{RESET}")
print(data)
else:
# Accumulate agent response (streaming text)
if isinstance(data, str):
accumulated_response += data
else:
accumulated_response += str(data)
elif event.type == "request_info" and isinstance(event.data, AgentExternalInputRequest):
request = event.data
# The agent_response from the request contains the structured response
agent_name = request.agent_name
agent_response = request.agent_response
# Print the agent's response
if agent_response:
msg_id = f"msg_{uuid.uuid4().hex[:32]}"
print(f"{CYAN}{agent_name.upper()}:{RESET} [{msg_id}]")
try:
parsed = json.loads(agent_response)
print(json.dumps(parsed))
except (json.JSONDecodeError, TypeError):
print(agent_response)
# Clear accumulated since we printed from the request
accumulated_response = ""
last_agent_name = agent_name
pending_request_id = event.request_id
print(f"\n{YELLOW}WORKFLOW:{RESET} Yield")
# Print any remaining accumulated response at end of stream
if accumulated_response:
# Try to identify which agent this came from based on content
msg_id = f"msg_{uuid.uuid4().hex[:32]}"
print(f"\nResponse: [{msg_id}]")
try:
parsed = json.loads(accumulated_response)
print(json.dumps(parsed))
except (json.JSONDecodeError, TypeError):
print(accumulated_response)
accumulated_response = ""
if not pending_request_id:
break
# Get next user input
user_input = input(f"\n{GREEN}INPUT:{RESET} ").strip() # noqa: ASYNC250
if not user_input:
print("Exiting...")
break
print()
print("\n" + "=" * 60)
print("Workflow Complete")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,79 @@
# Copyright (c) Microsoft. All rights reserved.
"""Ticketing plugin for CustomerSupport workflow."""
import uuid
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
# ANSI color codes
MAGENTA = "\033[35m"
RESET = "\033[0m"
class TicketStatus(Enum):
"""Status of a support ticket."""
OPEN = "open"
IN_PROGRESS = "in_progress"
RESOLVED = "resolved"
CLOSED = "closed"
@dataclass
class TicketItem:
"""A support ticket."""
id: str
subject: str = ""
description: str = ""
notes: str = ""
status: TicketStatus = TicketStatus.OPEN
class TicketingPlugin:
"""Mock ticketing plugin for customer support workflow."""
def __init__(self) -> None:
self._ticket_store: dict[str, TicketItem] = {}
def _trace(self, function_name: str) -> None:
print(f"\n{MAGENTA}FUNCTION: {function_name}{RESET}")
def get_ticket(self, id: str) -> TicketItem | None:
"""Retrieve a ticket by identifier from Azure DevOps."""
self._trace("get_ticket")
return self._ticket_store.get(id)
def create_ticket(self, subject: str, description: str, notes: str) -> str:
"""Create a ticket in Azure DevOps and return its identifier."""
self._trace("create_ticket")
ticket_id = uuid.uuid4().hex
ticket = TicketItem(
id=ticket_id,
subject=subject,
description=description,
notes=notes,
)
self._ticket_store[ticket_id] = ticket
return ticket_id
def resolve_ticket(self, id: str, resolution_summary: str) -> None:
"""Resolve an existing ticket in Azure DevOps given its identifier."""
self._trace("resolve_ticket")
if ticket := self._ticket_store.get(id):
ticket.status = TicketStatus.RESOLVED
def send_notification(self, id: str, email: str, cc: str, body: str) -> None:
"""Send an email notification to escalate ticket engagement."""
self._trace("send_notification")
def get_functions(self) -> list[Callable[..., object]]:
"""Return all plugin functions for registration."""
return [
self.get_ticket,
self.create_ticket,
self.resolve_ticket,
self.send_notification,
]
@@ -0,0 +1,164 @@
#
# This workflow demonstrates using multiple agents to provide automated
# troubleshooting steps to resolve common issues with escalation options.
#
# Example input:
# My PC keeps rebooting and I can't use it.
#
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_demo
actions:
# Interact with user until the issue has been resolved or
# a determination is made that a ticket is required.
- kind: InvokeAzureAgent
id: service_agent
conversationId: =System.ConversationId
agent:
name: SelfServiceAgent
input:
externalLoop:
when: |-
=Not(Local.ServiceParameters.IsResolved)
And
Not(Local.ServiceParameters.NeedsTicket)
output:
responseObject: Local.ServiceParameters
# All done if issue is resolved.
- kind: ConditionGroup
id: check_if_resolved
conditions:
- condition: =Local.ServiceParameters.IsResolved
id: test_if_resolved
actions:
- kind: GotoAction
id: end_when_resolved
actionId: all_done
# Create the ticket.
- kind: InvokeAzureAgent
id: ticket_agent
agent:
name: TicketingAgent
input:
arguments:
IssueDescription: =Local.ServiceParameters.IssueDescription
AttemptedResolutionSteps: =Local.ServiceParameters.AttemptedResolutionSteps
output:
responseObject: Local.TicketParameters
# Capture the attempted resolution steps.
- kind: SetVariable
id: capture_attempted_resolution
variable: Local.ResolutionSteps
value: =Local.ServiceParameters.AttemptedResolutionSteps
# Notify user of ticket identifier.
- kind: SendActivity
id: log_ticket
activity: "Created ticket #{Local.TicketParameters.TicketId}"
# Determine which team for which route the ticket.
- kind: InvokeAzureAgent
id: routing_agent
agent:
name: TicketRoutingAgent
input:
messages: =UserMessage(Local.ServiceParameters.IssueDescription)
output:
responseObject: Local.RoutingParameters
# Notify user of routing decision.
- kind: SendActivity
id: log_route
activity: Routing to {Local.RoutingParameters.TeamName}
- kind: ConditionGroup
id: check_routing
conditions:
- condition: =Local.RoutingParameters.TeamName = "Windows Support"
id: route_to_support
actions:
# Invoke the support agent to attempt to resolve the issue.
- kind: CreateConversation
id: conversation_support
conversationId: Local.SupportConversationId
- kind: InvokeAzureAgent
id: support_agent
conversationId: =Local.SupportConversationId
agent:
name: WindowsSupportAgent
input:
arguments:
IssueDescription: =Local.ServiceParameters.IssueDescription
AttemptedResolutionSteps: =Local.ServiceParameters.AttemptedResolutionSteps
externalLoop:
when: |-
=Not(Local.SupportParameters.IsResolved)
And
Not(Local.SupportParameters.NeedsEscalation)
output:
autoSend: true
responseObject: Local.SupportParameters
# Capture the attempted resolution steps.
- kind: SetVariable
id: capture_support_resolution
variable: Local.ResolutionSteps
value: =Local.SupportParameters.ResolutionSummary
# Check if the issue was resolved by support.
- kind: ConditionGroup
id: check_resolved
conditions:
# Resolve ticket
- condition: =Local.SupportParameters.IsResolved
id: handle_if_resolved
actions:
- kind: InvokeAzureAgent
id: resolution_agent
agent:
name: TicketResolutionAgent
input:
arguments:
TicketId: =Local.TicketParameters.TicketId
ResolutionSummary: =Local.SupportParameters.ResolutionSummary
- kind: GotoAction
id: end_when_solved
actionId: all_done
# Escalate the ticket by sending an email notification.
- kind: CreateConversation
id: conversation_escalate
conversationId: Local.EscalationConversationId
- kind: InvokeAzureAgent
id: escalate_agent
conversationId: =Local.EscalationConversationId
agent:
name: TicketEscalationAgent
input:
arguments:
TicketId: =Local.TicketParameters.TicketId
IssueDescription: =Local.ServiceParameters.IssueDescription
ResolutionSummary: =Local.ResolutionSteps
externalLoop:
when: =Not(Local.EscalationParameters.IsComplete)
output:
autoSend: true
responseObject: Local.EscalationParameters
# All done
- kind: EndWorkflow
id: all_done