Files
Copilot 88ea9d08c7 .NET: Update to OpenAI 2.9.1, Azure.AI.OpenAI 2.9.0-beta.1, Microsoft.Extensions.AI 10.4.0, and Azure.AI.Projects 2.0.0-beta.2 (#4613)
* Initial plan

* Update code for Microsoft.Extensions.AI.Abstractions 10.4.0 breaking changes

- Rename FunctionApprovalRequestContent → ToolApprovalRequestContent
- Rename FunctionApprovalResponseContent → ToolApprovalResponseContent
- Rename UserInputRequestContent → ToolApprovalRequestContent
- Rename UserInputResponseContent → ToolApprovalResponseContent
- Update .FunctionCall property → .ToolCall with FunctionCallContent casts where needed
- Update .Id property → .RequestId on the renamed types
- Rename FunctionApprovalRequestEventGenerator → ToolApprovalRequestEventGenerator
- Rename FunctionApprovalResponseEventGenerator → ToolApprovalResponseEventGenerator

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update OpenAI 2.9.1, ME.AI 10.4.0, fix breaking API changes

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix remaining ME.AI 10.4.0 breaking changes: MCP approval types, .Output→.Outputs

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Use pattern matching with `when` for ToolApprovalRequestContent/FunctionCallContent

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Update Azure.AI.OpenAI to 2.9.0-beta.1

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix remaining GetResponsesClient(model) build failures for Azure.AI.OpenAI 2.9.0-beta.1

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Address review feedback: remove redundant type checks in TestRequestAgent.cs and fix error message in AIAgentHostExecutor.cs

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Update Azure.AI.Projects to 2.0.0-beta.2 with namespace migration

- Azure.AI.Projects 2.0.0-beta.1 → 2.0.0-beta.2
- Azure.AI.Projects.OpenAI → Azure.AI.Extensions.OpenAI (transitive)
- Agent types moved to Azure.AI.Projects.Agents namespace
- AgentRecord.Versions.Latest → AgentRecord.GetLatestVersion()
- OpenAPIFunctionDefinition → OpenApiFunctionDefinition
- BingCustomSearchToolParameters → BingCustomSearchToolOptions
- MemorySearchPreviewTool.UpdateDelay → UpdateDelayInSecs
- Azure.Identity 1.17.1 → 1.19.0
- Microsoft.Identity.Client.Extensions.Msal 4.78.0 → 4.83.1

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Fix remaining type renames for Azure.AI.Projects 2.0.0-beta.2

- BrowserAutomationToolParameters → BrowserAutomationToolOptions
- MemoryUpdateOptions.UpdateDelay stays as UpdateDelay (not renamed)
- WaitForMemoriesUpdateAsync parameter order: pollingInterval before options
- AIProjectAgentsOperations → AgentsClient

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Fix format errors and OpenTelemetry test for ME.AI 10.4.0

- Remove unused 'using Azure.AI.Extensions.OpenAI' and fix import ordering
  in Agent_With_AzureAIProject/Program.cs
- Update OpenTelemetryAgentTests: gen_ai.tool.definitions is now always
  emitted regardless of EnableSensitiveData per ME.AI 10.4.0 change
  (dotnet/extensions#7346). Tool definitions are not considered sensitive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix GetRepoFolder() to work in git worktrees

Use 'workflow-samples' directory as repo root marker instead of '.git',
which fails in worktrees (.git is a file) and also matches too early
when a '.github' folder exists in subdirectories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix formatting: remove unused usings and fix import ordering

dotnet format applied across 59 impacted projects. Primarily removes
unnecessary 'using Azure.AI.Projects' where Azure.AI.Projects.Agents
provides all needed types, and fixes import ordering per editorconfig.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Disable AzureAIAgentsPersistent integration tests (#4769)

Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent
which was removed in ME.AI 10.4.0 (renamed to ToolApprovalResponseContent), causing
TypeLoadException at runtime. Mark all 6 test classes with IntegrationDisabled trait
until Persistent ships a version targeting ME.AI 10.4.0+.

Upstream fix: https://github.com/Azure/azure-sdk-for-net/pull/56929

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add README with compatibility note for AzureAI.Persistent (#4769)

Documents that Azure.AI.Agents.Persistent 1.2.0-beta.9 is only compatible
with ME.AI ≤10.3.0 and OpenAI ≤2.8.0 due to type renames in ME.AI 10.4.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix file encoding: restore UTF-8 BOM on Persistent test files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Mark AzureAI.Persistent as IsPackable=false (#4769)

Prevent shipping until Azure.AI.Agents.Persistent targets ME.AI 10.4.0+.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Moving IsPackable after import

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-20 14:29:29 +00:00

445 lines
20 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using OpenAI.Responses;
using Shared.Foundry;
using Shared.Workflows;
namespace Demo.Workflows.Declarative.CustomerSupport;
/// <summary>
/// This workflow demonstrates using multiple agents to provide automated
/// troubleshooting steps to resolve common issues with escalation options.
/// </summary>
/// <remarks>
/// See the README.md file in the parent folder (../README.md) for detailed
/// information about the configuration required to run this sample.
/// </remarks>
internal sealed class Program
{
public static async Task Main(string[] args)
{
// Initialize configuration
IConfiguration configuration = Application.InitializeConfig();
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
// Create the ticketing plugin (mock functionality)
TicketingPlugin plugin = new();
// Ensure sample agents exist in Foundry.
await CreateAgentsAsync(foundryEndpoint, configuration, plugin);
// Get input from command line or console
string workflowInput = Application.GetInput(args);
// Create the workflow factory. This class demonstrates how to initialize a
// declarative workflow from a YAML file. Once the workflow is created, it
// can be executed just like any regular workflow.
WorkflowFactory workflowFactory =
new("CustomerSupport.yaml", foundryEndpoint)
{
Functions =
[
AIFunctionFactory.Create(plugin.CreateTicket),
AIFunctionFactory.Create(plugin.GetTicket),
AIFunctionFactory.Create(plugin.ResolveTicket),
AIFunctionFactory.Create(plugin.SendNotification),
]
};
// Execute the workflow: The WorkflowRunner demonstrates how to execute
// a workflow, handle the workflow events, and providing external input.
// This also includes the ability to checkpoint workflow state and how to
// resume execution.
WorkflowRunner runner = new();
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
}
private static async Task CreateAgentsAsync(Uri foundryEndpoint, IConfiguration configuration, TicketingPlugin plugin)
{
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(foundryEndpoint, new DefaultAzureCredential());
await aiProjectClient.CreateAgentAsync(
agentName: "SelfServiceAgent",
agentDefinition: DefineSelfServiceAgent(configuration),
agentDescription: "Service agent for CustomerSupport workflow");
await aiProjectClient.CreateAgentAsync(
agentName: "TicketingAgent",
agentDefinition: DefineTicketingAgent(configuration, plugin),
agentDescription: "Ticketing agent for CustomerSupport workflow");
await aiProjectClient.CreateAgentAsync(
agentName: "TicketRoutingAgent",
agentDefinition: DefineTicketRoutingAgent(configuration, plugin),
agentDescription: "Routing agent for CustomerSupport workflow");
await aiProjectClient.CreateAgentAsync(
agentName: "WindowsSupportAgent",
agentDefinition: DefineWindowsSupportAgent(configuration, plugin),
agentDescription: "Windows support agent for CustomerSupport workflow");
await aiProjectClient.CreateAgentAsync(
agentName: "TicketResolutionAgent",
agentDefinition: DefineResolutionAgent(configuration, plugin),
agentDescription: "Resolution agent for CustomerSupport workflow");
await aiProjectClient.CreateAgentAsync(
agentName: "TicketEscalationAgent",
agentDefinition: TicketEscalationAgent(configuration, plugin),
agentDescription: "Escalate agent for human support");
}
private static PromptAgentDefinition DefineSelfServiceAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
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.
""",
TextOptions =
new ResponseTextOptions
{
TextFormat =
ResponseTextFormat.CreateJsonSchemaFormat(
"TaskEvaluation",
BinaryData.FromString(
"""
{
"type": "object",
"properties": {
"IsResolved": {
"type": "boolean",
"description": "True if the user issue/ask has been resolved."
},
"NeedsTicket": {
"type": "boolean",
"description": "True if the user issue/ask requires that a ticket be filed."
},
"IssueDescription": {
"type": "string",
"description": "A concise description of the issue."
},
"AttemptedResolutionSteps": {
"type": "string",
"description": "An outline of the steps taken to attempt resolution."
}
},
"required": ["IsResolved", "NeedsTicket", "IssueDescription", "AttemptedResolutionSteps"],
"additionalProperties": false
}
"""),
jsonSchemaFormatDescription: null,
jsonSchemaIsStrict: true),
}
};
private static PromptAgentDefinition DefineTicketingAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
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.
""",
Tools =
{
AIFunctionFactory.Create(plugin.CreateTicket).AsOpenAIResponseTool()
},
StructuredInputs =
{
["IssueDescription"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "A concise description of the issue.",
},
["AttemptedResolutionSteps"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "An outline of the steps taken to attempt resolution.",
}
},
TextOptions =
new ResponseTextOptions
{
TextFormat =
ResponseTextFormat.CreateJsonSchemaFormat(
"TaskEvaluation",
BinaryData.FromString(
"""
{
"type": "object",
"properties": {
"TicketId": {
"type": "string",
"description": "The identifier of the ticket created in response to the user issue."
},
"TicketSummary": {
"type": "string",
"description": "The summary of the ticket created in response to the user issue."
}
},
"required": ["TicketId", "TicketSummary"],
"additionalProperties": false
}
"""),
jsonSchemaFormatDescription: null,
jsonSchemaIsStrict: true),
}
};
private static PromptAgentDefinition DefineTicketRoutingAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
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
""",
Tools =
{
AIFunctionFactory.Create(plugin.GetTicket).AsOpenAIResponseTool(),
},
TextOptions =
new ResponseTextOptions
{
TextFormat =
ResponseTextFormat.CreateJsonSchemaFormat(
"TaskEvaluation",
BinaryData.FromString(
"""
{
"type": "object",
"properties": {
"TeamName": {
"type": "string",
"description": "The name of the team to route the issue"
}
},
"required": ["TeamName"],
"additionalProperties": false
}
"""),
jsonSchemaFormatDescription: null,
jsonSchemaIsStrict: true),
}
};
private static PromptAgentDefinition DefineWindowsSupportAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
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}}
""",
StructuredInputs =
{
["IssueDescription"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "A concise description of the issue.",
},
["AttemptedResolutionSteps"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "An outline of the steps taken to attempt resolution.",
}
},
Tools =
{
AIFunctionFactory.Create(plugin.GetTicket).AsOpenAIResponseTool(),
},
TextOptions =
new ResponseTextOptions
{
TextFormat =
ResponseTextFormat.CreateJsonSchemaFormat(
"TaskEvaluation",
BinaryData.FromString(
"""
{
"type": "object",
"properties": {
"IsResolved": {
"type": "boolean",
"description": "True if the user issue/ask has been resolved."
},
"NeedsEscalation": {
"type": "boolean",
"description": "True resolution could not be achieved and the issue/ask requires escalation."
},
"ResolutionSummary": {
"type": "string",
"description": "The summary of the steps that led to resolution."
}
},
"required": ["IsResolved", "NeedsEscalation", "ResolutionSummary"],
"additionalProperties": false
}
"""),
jsonSchemaFormatDescription: null,
jsonSchemaIsStrict: true),
}
};
private static PromptAgentDefinition DefineResolutionAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
"""
Resolve the following ticket in Azure DevOps.
Always include the resolution details.
- Ticket ID: #{{TicketId}}
- Resolution Summary: {{ResolutionSummary}}
""",
Tools =
{
AIFunctionFactory.Create(plugin.ResolveTicket).AsOpenAIResponseTool(),
},
StructuredInputs =
{
["TicketId"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "The identifier of the ticket being resolved.",
},
["ResolutionSummary"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "The steps taken to resolve the issue.",
}
}
};
private static PromptAgentDefinition TicketEscalationAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
"""
You escalate the provided issue to human support team by sending an email if the issue is not resolved.
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.
""",
Tools =
{
AIFunctionFactory.Create(plugin.GetTicket).AsOpenAIResponseTool(),
AIFunctionFactory.Create(plugin.SendNotification).AsOpenAIResponseTool(),
},
StructuredInputs =
{
["TicketId"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "The identifier of the ticket being escalated.",
},
["IssueDescription"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "A concise description of the issue.",
},
["ResolutionSummary"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""unknown"""),
Description = "An outline of the steps taken to attempt resolution.",
}
},
TextOptions =
new ResponseTextOptions
{
TextFormat =
ResponseTextFormat.CreateJsonSchemaFormat(
"TaskEvaluation",
BinaryData.FromString(
"""
{
"type": "object",
"properties": {
"IsComplete": {
"type": "boolean",
"description": "Has the email been sent and no more user input is required."
},
"UserMessage": {
"type": "string",
"description": "A natural language message to the user."
}
},
"required": ["IsComplete", "UserMessage"],
"additionalProperties": false
}
"""),
jsonSchemaFormatDescription: null,
jsonSchemaIsStrict: true),
}
};
}