chore: import upstream snapshot with attribution
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) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
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) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace WorkflowCustomAgentExecutorsSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample demonstrates how to create custom executors for AI agents.
|
||||
/// This is useful when you want more control over the agent's behaviors in a workflow.
|
||||
///
|
||||
/// In this example, we create two custom executors:
|
||||
/// 1. SloganWriterExecutor: An AI agent that generates slogans based on a given task.
|
||||
/// 2. FeedbackExecutor: An AI agent that provides feedback on the generated slogans.
|
||||
/// (These two executors manage the agent instances and their conversation threads.)
|
||||
///
|
||||
/// The workflow alternates between these two executors until the slogan meets a certain
|
||||
/// quality threshold or a maximum number of attempts is reached.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// - An Azure OpenAI chat completion deployment that supports structured outputs must be configured.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure AI Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Create the executors
|
||||
var sloganWriter = new SloganWriterExecutor("SloganWriter", aiProjectClient, deploymentName);
|
||||
var feedbackProvider = new FeedbackExecutor("FeedbackProvider", aiProjectClient, deploymentName);
|
||||
|
||||
// Build the workflow by adding executors and connecting them
|
||||
var workflow = new WorkflowBuilder(sloganWriter)
|
||||
.AddEdge(sloganWriter, feedbackProvider)
|
||||
.AddEdge(feedbackProvider, sloganWriter)
|
||||
.WithOutputFrom(feedbackProvider)
|
||||
.Build();
|
||||
|
||||
// Execute the workflow
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input: "Create a slogan for a new electric SUV that is affordable and fun to drive.");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
if (evt is SloganGeneratedEvent or FeedbackEvent)
|
||||
{
|
||||
// Custom events to allow us to monitor the progress of the workflow.
|
||||
Console.WriteLine($"{evt}");
|
||||
}
|
||||
|
||||
if (evt is WorkflowOutputEvent outputEvent)
|
||||
{
|
||||
Console.WriteLine($"{outputEvent}");
|
||||
}
|
||||
|
||||
if (evt is WorkflowErrorEvent errorEvent)
|
||||
{
|
||||
Console.WriteLine($"Workflow error: {errorEvent.Exception?.Message}");
|
||||
Console.WriteLine($"Details: {errorEvent.Exception}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing the output of the slogan writer agent.
|
||||
/// </summary>
|
||||
public sealed class SloganResult
|
||||
{
|
||||
[JsonPropertyName("task")]
|
||||
public required string Task { get; set; }
|
||||
|
||||
[JsonPropertyName("slogan")]
|
||||
public required string Slogan { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing the output of the feedback agent.
|
||||
/// </summary>
|
||||
public sealed class FeedbackResult
|
||||
{
|
||||
[JsonPropertyName("comments")]
|
||||
public string Comments { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("rating")]
|
||||
public int Rating { get; set; }
|
||||
|
||||
[JsonPropertyName("actions")]
|
||||
public string Actions { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom event to indicate that a slogan has been generated.
|
||||
/// </summary>
|
||||
internal sealed class SloganGeneratedEvent(SloganResult sloganResult) : WorkflowEvent(sloganResult)
|
||||
{
|
||||
public override string ToString() => $"Slogan: {sloganResult.Slogan}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom executor that uses an AI agent to generate slogans based on a given task.
|
||||
/// Note that this executor has two message handlers:
|
||||
/// 1. HandleAsync(string message): Handles the initial task to create a slogan.
|
||||
/// 2. HandleAsync(Feedback message): Handles feedback to improve the slogan.
|
||||
/// </summary>
|
||||
internal sealed partial class SloganWriterExecutor : Executor
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
private AgentSession? _session;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SloganWriterExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="chatClient">The AI project client to use for the AI agent.</param>
|
||||
/// <param name="model">The model deployment name.</param>
|
||||
public SloganWriterExecutor(string id, AIProjectClient chatClient, string model) : base(id)
|
||||
{
|
||||
ChatClientAgentOptions agentOptions = new()
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = model,
|
||||
Instructions = "You are a professional slogan writer. You will be given a task to create a slogan.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<SloganResult>()
|
||||
}
|
||||
};
|
||||
|
||||
this._agent = chatClient.AsAIAgent(agentOptions);
|
||||
}
|
||||
|
||||
[MessageHandler]
|
||||
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._session ??= await this._agent.CreateSessionAsync(cancellationToken);
|
||||
|
||||
var result = await this._agent.RunAsync(message, this._session, cancellationToken: cancellationToken);
|
||||
|
||||
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
|
||||
|
||||
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken);
|
||||
return sloganResult;
|
||||
}
|
||||
|
||||
[MessageHandler]
|
||||
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var feedbackMessage = $"""
|
||||
Here is the feedback on your previous slogan:
|
||||
Comments: {message.Comments}
|
||||
Rating: {message.Rating}
|
||||
Suggested Actions: {message.Actions}
|
||||
|
||||
Please use this feedback to improve your slogan.
|
||||
""";
|
||||
|
||||
var result = await this._agent.RunAsync(feedbackMessage, this._session, cancellationToken: cancellationToken);
|
||||
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
|
||||
|
||||
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken);
|
||||
return sloganResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom event to indicate that feedback has been provided.
|
||||
/// </summary>
|
||||
internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEvent(feedbackResult)
|
||||
{
|
||||
private readonly JsonSerializerOptions _options = new() { WriteIndented = true };
|
||||
public override string ToString() => $"Feedback:\n{JsonSerializer.Serialize(feedbackResult, this._options)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom executor that uses an AI agent to provide feedback on a slogan.
|
||||
/// </summary>
|
||||
[SendsMessage(typeof(FeedbackResult))]
|
||||
[YieldsOutput(typeof(string))]
|
||||
internal sealed partial class FeedbackExecutor : Executor<SloganResult>
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
private AgentSession? _session;
|
||||
|
||||
public int MinimumRating { get; init; } = 8;
|
||||
|
||||
public int MaxAttempts { get; init; } = 3;
|
||||
|
||||
private int _attempts;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FeedbackExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="chatClient">The AI project client to use for the AI agent.</param>
|
||||
/// <param name="model">The model deployment name.</param>
|
||||
public FeedbackExecutor(string id, AIProjectClient chatClient, string model) : base(id)
|
||||
{
|
||||
ChatClientAgentOptions agentOptions = new()
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = model,
|
||||
Instructions = "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<FeedbackResult>()
|
||||
}
|
||||
};
|
||||
|
||||
this._agent = chatClient.AsAIAgent(agentOptions);
|
||||
}
|
||||
|
||||
public override async ValueTask HandleAsync(SloganResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._session ??= await this._agent.CreateSessionAsync(cancellationToken);
|
||||
|
||||
var sloganMessage = $"""
|
||||
Here is a slogan for the task '{message.Task}':
|
||||
Slogan: {message.Slogan}
|
||||
Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
|
||||
""";
|
||||
|
||||
var response = await this._agent.RunAsync(sloganMessage, this._session, cancellationToken: cancellationToken);
|
||||
var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
|
||||
|
||||
await context.AddEventAsync(new FeedbackEvent(feedback), cancellationToken);
|
||||
|
||||
if (feedback.Rating >= this.MinimumRating)
|
||||
{
|
||||
await context.YieldOutputAsync($"The following slogan was accepted:\n\n{message.Slogan}", cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._attempts >= this.MaxAttempts)
|
||||
{
|
||||
await context.YieldOutputAsync($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}", cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await context.SendMessageAsync(feedback, cancellationToken: cancellationToken);
|
||||
this._attempts++;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user