Enable more analyzers and various code tweaks/cleanup (#738)

This commit is contained in:
Stephen Toub
2025-09-17 21:48:25 -04:00
committed by GitHub
Unverified
parent f3264966ff
commit 5e5761b288
326 changed files with 2402 additions and 3642 deletions
+38 -37
View File
@@ -124,10 +124,12 @@ dotnet_diagnostic.CA1063.severity = warning # Implement IDisposable correctly
dotnet_diagnostic.CA1064.severity = warning # Exceptions should be public
dotnet_diagnostic.CA1416.severity = warning # Validate platform compatibility
dotnet_diagnostic.CA1508.severity = warning # Avoid dead conditional code
dotnet_diagnostic.CA1805.severity = warning # Member is explicitly initialized to its default value
dotnet_diagnostic.CA1822.severity = suggestion # Member does not access instance data and can be marked as static
dotnet_diagnostic.CA1852.severity = warning # Sealed classes
dotnet_diagnostic.CA1859.severity = warning # Use concrete types when possible for improved performance
dotnet_diagnostic.CA1860.severity = warning # Prefer comparing 'Count' to 0 rather than using 'Any()', both for clarity and for performance
dotnet_diagnostic.CA2000.severity = warning # Call System.IDisposable.Dispose on object before all references to it are out of scope
dotnet_diagnostic.CA2007.severity = warning # Do not directly await a Task
dotnet_diagnostic.CA2201.severity = warning # Exception type System.Exception is not sufficiently specific
dotnet_diagnostic.IDE0001.severity = warning # Simplify name
@@ -152,9 +154,39 @@ dotnet_diagnostic.IDE0082.severity = warning # Convert typeof to nameof
dotnet_diagnostic.IDE0090.severity = warning # Simplify new expression
dotnet_diagnostic.IDE0161.severity = warning # Use file-scoped namespace
dotnet_diagnostic.VSTHRD111.severity = error # Use .ConfigureAwait(bool)
dotnet_diagnostic.VSTHRD200.severity = warning # Use Async suffix for async methods
dotnet_diagnostic.RCS1021.severity = warning # Use expression-bodied lambda.
dotnet_diagnostic.RCS1061.severity = warning # Merge 'if' with nested 'if'.
dotnet_diagnostic.RCS1069.severity = warning # Remove unnecessary case label.
dotnet_diagnostic.RCS1077.severity = warning # Optimize LINQ method call.
dotnet_diagnostic.RCS1118.severity = warning # Mark local variable as const.
dotnet_diagnostic.RCS1124.severity = warning # Inline local variable.
dotnet_diagnostic.RCS1129.severity = warning # Remove redundant field initialization.
dotnet_diagnostic.RCS1146.severity = warning # Use conditional access.
dotnet_diagnostic.RCS1170.severity = warning # Use read-only auto-implemented property.
dotnet_diagnostic.RCS1173.severity = warning # Use coalesce expression instead of 'if'.
dotnet_diagnostic.RCS1186.severity = warning # Use Regex instance instead of static method.
dotnet_diagnostic.RCS1188.severity = warning # Remove redundant auto-property initialization.
dotnet_diagnostic.RCS1197.severity = suggestion # Optimize StringBuilder.AppendLine call.
dotnet_diagnostic.RCS1201.severity = warning # Use method chaining.
dotnet_diagnostic.IDE0001.severity = warning # Simplify name
dotnet_diagnostic.IDE0002.severity = warning # Simplify member access
dotnet_diagnostic.IDE0004.severity = warning # Remove unnecessary cast
dotnet_diagnostic.IDE0035.severity = warning # Remove unreachable code
dotnet_diagnostic.IDE0051.severity = warning # Remove unused private member
dotnet_diagnostic.IDE0052.severity = warning # Remove unread private member
dotnet_diagnostic.IDE0059.severity = warning # Unnecessary assignment of a value
dotnet_diagnostic.IDE0110.severity = warning # Remove unnecessary discards
dotnet_diagnostic.IDE0032.severity = warning # Use auto property
dotnet_diagnostic.IDE0047.severity = warning # Parentheses can be removed
dotnet_diagnostic.IDE1006.severity = error # Naming rule violations
# Suppressed diagnostics
dotnet_diagnostic.CA1002.severity = none # Change 'List<string>' in '...' to use 'Collection<T>' ...
dotnet_diagnostic.CA1031.severity = suggestion # Do not catch general exception types
dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types
dotnet_diagnostic.CA1032.severity = none # We're using RCS1194 which seems to cover more ctors
dotnet_diagnostic.CA1034.severity = none # Do not nest type. Alternatively, change its accessibility so that it is not externally visible
dotnet_diagnostic.CA1062.severity = none # Disable null check, C# already does it for us
@@ -165,51 +197,35 @@ dotnet_diagnostic.CA1508.severity = none # Avoid dead conditional code. Too many
dotnet_diagnostic.CA1510.severity = none # ArgumentNullException.Throw
dotnet_diagnostic.CA1512.severity = none # ArgumentOutOfRangeException.Throw
dotnet_diagnostic.CA1515.severity = none # Making public types from exes internal
dotnet_diagnostic.CA1805.severity = none # Member is explicitly initialized to its default value
dotnet_diagnostic.CA1822.severity = none # Member does not access instance data and can be marked as static
dotnet_diagnostic.CA1848.severity = none # For improved performance, use the LoggerMessage delegates
dotnet_diagnostic.CA1849.severity = none # Use async equivalent; analyzer is currently noisy
dotnet_diagnostic.CA1865.severity = none # StartsWith(char)
dotnet_diagnostic.CA1867.severity = none # EndsWith(char)
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
dotnet_diagnostic.CS1998.severity = none # async method lacks 'await' operators and will run synchronously
dotnet_diagnostic.CA2000.severity = suggestion # Call System.IDisposable.Dispose on object before all references to it are out of scope
dotnet_diagnostic.CA2225.severity = none # Operator overloads have named alternates
dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing the property setter
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
dotnet_diagnostic.CA2263.severity = suggestion # Use generic overload
dotnet_diagnostic.CS1998.severity = suggestion # async method lacks 'await' operators and will run synchronously
dotnet_diagnostic.CA5394.severity = none # Do not use insecure sources of randomness
dotnet_diagnostic.VSTHRD003.severity = none # Waiting on thread from another context
dotnet_diagnostic.VSTHRD103.severity = none # Use async equivalent; analyzer is currently noisy
dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
dotnet_diagnostic.VSTHRD200.severity = none # Use Async suffix for async methods
dotnet_diagnostic.xUnit1004.severity = none # Test methods should not be skipped. Remove the Skip property to start running the test again.
dotnet_diagnostic.RCS1021.severity = none # Use expression-bodied lambda.
dotnet_diagnostic.RCS1032.severity = none # Remove redundant parentheses.
dotnet_diagnostic.RCS1061.severity = none # Merge 'if' with nested 'if'.
dotnet_diagnostic.RCS1069.severity = none # Remove unnecessary case label.
dotnet_diagnostic.RCS1074.severity = none # Remove redundant constructor.
dotnet_diagnostic.RCS1077.severity = none # Optimize LINQ method call.
dotnet_diagnostic.RCS1118.severity = none # Mark local variable as const.
dotnet_diagnostic.RCS1124.severity = none # Inline local variable.
dotnet_diagnostic.RCS1129.severity = none # Remove redundant field initialization.
dotnet_diagnostic.RCS1140.severity = none # Add exception to documentation comment.
dotnet_diagnostic.RCS1141.severity = none # Add 'param' element to documentation comment.
dotnet_diagnostic.RCS1142.severity = none # Add 'typeparam' element to documentation comment.
dotnet_diagnostic.RCS1146.severity = none # Use conditional access.
dotnet_diagnostic.RCS1151.severity = none # Remove redundant cast.
dotnet_diagnostic.RCS1158.severity = none # Static member in generic type should use a type parameter.
dotnet_diagnostic.RCS1161.severity = none # Enum should declare explicit value
dotnet_diagnostic.RCS1163.severity = none # Unused parameter 'foo'.
dotnet_diagnostic.RCS1170.severity = none # Use read-only auto-implemented property.
dotnet_diagnostic.RCS1173.severity = none # Use coalesce expression instead of 'if'.
dotnet_diagnostic.RCS1181.severity = none # Convert comment to documentation comment.
dotnet_diagnostic.RCS1186.severity = none # Use Regex instance instead of static method.
dotnet_diagnostic.RCS1188.severity = none # Remove redundant auto-property initialization.
dotnet_diagnostic.RCS1189.severity = none # Add region name to #endregion.
dotnet_diagnostic.RCS1197.severity = none # Optimize StringBuilder.AppendLine call.
dotnet_diagnostic.RCS1201.severity = none # Use method chaining.
dotnet_diagnostic.RCS1205.severity = none # Order named arguments according to the order of parameters.
dotnet_diagnostic.RCS1212.severity = none # Remove redundant assignment.
dotnet_diagnostic.RCS1217.severity = none # Convert interpolated string to concatenation.
@@ -220,42 +236,27 @@ dotnet_diagnostic.RCS1234.severity = none # Enum duplicate value
dotnet_diagnostic.RCS1238.severity = none # Avoid nested ?: operators.
dotnet_diagnostic.RCS1241.severity = none # Implement IComparable when implementing IComparable<T><T>.
dotnet_diagnostic.IDE0001.severity = none # Simplify name
dotnet_diagnostic.IDE0002.severity = none # Simplify member access
dotnet_diagnostic.IDE0004.severity = none # Remove unnecessary cast
dotnet_diagnostic.IDE0010.severity = none # Populate switch
dotnet_diagnostic.IDE0021.severity = none # Use block body for constructors
dotnet_diagnostic.IDE0022.severity = none # Use block body for methods
dotnet_diagnostic.IDE0024.severity = none # Use block body for operator
dotnet_diagnostic.IDE0035.severity = none # Remove unreachable code
dotnet_diagnostic.IDE0051.severity = none # Remove unused private member
dotnet_diagnostic.IDE0052.severity = none # Remove unread private member
dotnet_diagnostic.IDE0058.severity = none # Remove unused expression value
dotnet_diagnostic.IDE0059.severity = none # Unnecessary assignment of a value
dotnet_diagnostic.IDE0060.severity = none # Remove unused parameter
dotnet_diagnostic.IDE0061.severity = none # Use block body for local function
dotnet_diagnostic.IDE0079.severity = none # Remove unnecessary suppression.
dotnet_diagnostic.IDE0080.severity = none # Remove unnecessary suppression operator.
dotnet_diagnostic.IDE0100.severity = none # Remove unnecessary equality operator
dotnet_diagnostic.IDE0110.severity = none # Remove unnecessary discards
dotnet_diagnostic.IDE0130.severity = none # Namespace does not match folder structure
dotnet_diagnostic.IDE0290.severity = none # Use primary constructor
dotnet_diagnostic.IDE0032.severity = none # Use auto property
dotnet_diagnostic.IDE0160.severity = none # Use block-scoped namespace
dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
dotnet_diagnostic.IDE0042.severity = none # Variable declaration can be deconstructed
dotnet_diagnostic.IDE0046.severity = suggestion # If statement can be simplified
dotnet_diagnostic.IDE0056.severity = suggestion # Indexing can be simplified
dotnet_diagnostic.IDE0057.severity = suggestion # Substring can be simplified
dotnet_diagnostic.IDE0079.severity = none # Remove unnecessary suppression
dotnet_diagnostic.IDE0290.severity = none # Use primary constructor
dotnet_diagnostic.IDE0046.severity = none # if statement can be simplified
dotnet_diagnostic.IDE0057.severity = none # Substring can be simplified
dotnet_diagnostic.IDE0042.severity = none # Variable declaration can be deconstructed
dotnet_diagnostic.IDE0047.severity = none # Parentheses can be removed
dotnet_diagnostic.CA2007.severity = error # Do not directly await a Task
dotnet_diagnostic.VSTHRD111.severity = error # Use .ConfigureAwait(bool)
dotnet_diagnostic.IDE1006.severity = error # Naming rule violations
# Testing
dotnet_diagnostic.Moq1400.severity = none # Explicitly choose a mocking behavior instead of relying on the default (Loose) behavior
+1 -1
View File
@@ -37,11 +37,11 @@
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.9" />
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.9" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.9" />
<PackageVersion Include="System.Threading.Channels" Version="9.0.9" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.9" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
@@ -12,7 +12,6 @@ internal sealed class HostClientAgent
{
internal HostClientAgent(ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory;
this._logger = loggerFactory.CreateLogger("HostClientAgent");
}
@@ -23,7 +22,7 @@ internal sealed class HostClientAgent
this._logger.LogInformation("Initializing Agent Framework agent with model: {ModelId}", modelId);
// Connect to the remote agents via A2A
var createAgentTasks = agentUrls.Select(agentUrl => this.CreateAgentAsync(agentUrl));
var createAgentTasks = agentUrls.Select(CreateAgentAsync);
var agents = await Task.WhenAll(createAgentTasks);
var tools = agents.Select(agent => (AITool)agent.AsAIFunction()).ToList();
@@ -45,10 +44,9 @@ internal sealed class HostClientAgent
public AIAgent? Agent { get; private set; }
#region private
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
private async Task<AIAgent> CreateAgentAsync(string agentUri)
private static async Task<AIAgent> CreateAgentAsync(string agentUri)
{
var url = new Uri(agentUri);
var httpClient = new HttpClient
@@ -57,7 +57,7 @@ public static class Program
continue;
}
if (message == ":q" || message == "quit")
if (message is ":q" or "quit")
{
break;
}
@@ -20,10 +20,7 @@ public class Product
this.Price = price;
}
public decimal TotalPrice()
{
return this.Quantity * this.Price; // Total price for this product
}
public decimal TotalPrice() => this.Quantity * this.Price; // Total price for this product
}
public class Invoice
@@ -43,82 +40,78 @@ public class Invoice
this.Products = products;
}
public decimal TotalInvoicePrice()
{
return this.Products.Sum(product => product.TotalPrice()); // Total price of all products in the invoice
}
public decimal TotalInvoicePrice() => this.Products.Sum(product => product.TotalPrice()); // Total price of all products in the invoice
}
public class InvoiceQuery
{
private readonly List<Invoice> _invoices;
private static readonly Random s_random = new();
public InvoiceQuery()
{
// Extended mock data with quantities and prices
this._invoices =
[
new("TICKET-XYZ987", "INV789", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("TICKET-XYZ987", "INV789", "Contoso", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 150, 10.00m),
new("Hats", 200, 15.00m),
new("Glasses", 300, 5.00m)
}),
new("TICKET-XYZ111", "INV111", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ111", "INV111", "XStore", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 2500, 12.00m),
new("Hats", 1500, 8.00m),
new("Glasses", 200, 20.00m)
}),
new("TICKET-XYZ222", "INV222", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ222", "INV222", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 1200, 14.00m),
new("Hats", 800, 7.00m),
new("Glasses", 500, 25.00m)
}),
new("TICKET-XYZ333", "INV333", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ333", "INV333", "Contoso", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 400, 11.00m),
new("Hats", 600, 15.00m),
new("Glasses", 700, 5.00m)
}),
new("TICKET-XYZ444", "INV444", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ444", "INV444", "XStore", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 800, 10.00m),
new("Hats", 500, 18.00m),
new("Glasses", 300, 22.00m)
}),
new("TICKET-XYZ555", "INV555", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ555", "INV555", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 1100, 9.00m),
new("Hats", 900, 12.00m),
new("Glasses", 1200, 15.00m)
}),
new("TICKET-XYZ666", "INV666", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ666", "INV666", "Contoso", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 2500, 8.00m),
new("Hats", 1200, 10.00m),
new("Glasses", 1000, 6.00m)
}),
new("TICKET-XYZ777", "INV777", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ777", "INV777", "XStore", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 1900, 13.00m),
new("Hats", 1300, 16.00m),
new("Glasses", 800, 19.00m)
}),
new("TICKET-XYZ888", "INV888", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ888", "INV888", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 2200, 11.00m),
new("Hats", 1700, 8.50m),
new("Glasses", 600, 21.00m)
}),
new("TICKET-XYZ999", "INV999", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
]),
new("TICKET-XYZ999", "INV999", "Contoso", GetRandomDateWithinLastTwoMonths(),
[
new("T-Shirts", 1400, 10.50m),
new("Hats", 1100, 9.00m),
new("Glasses", 950, 12.00m)
})
])
];
}
@@ -132,9 +125,7 @@ public class InvoiceQuery
// Generate a random number of days between 0 and the total number of days in the range
int totalDays = (endDate - startDate).Days;
#pragma warning disable CA5394 // Do not use insecure randomness
int randomDays = s_random.Next(0, totalDays + 1); // +1 to include the end date
#pragma warning restore CA5394 // Do not use insecure randomness
int randomDays = Random.Shared.Next(0, totalDays + 1); // +1 to include the end date
// Return the random date
return startDate.AddDays(randomDays);
@@ -28,12 +28,12 @@ internal static class HttpActorProcessor
return GetResult(response);
}
if (streaming == true)
if (streaming is true)
{
return new ActorUpdateStreamingResult(responseHandle);
}
if (blocking == true)
if (blocking is true)
{
response = await responseHandle.GetResponseAsync(cancellationToken);
return GetResult(response);
@@ -44,7 +44,7 @@ internal static class HttpActorProcessor
ActorId = actorId,
MessageId = messageId,
Status = RequestStatus.Pending,
Data = JsonDocument.Parse("{}").RootElement
Data = JsonSerializer.Deserialize<JsonElement>("{}"),
});
}
@@ -64,12 +64,12 @@ internal static class HttpActorProcessor
return GetResult(response);
}
if (streaming == true)
if (streaming is true)
{
return new ActorUpdateStreamingResult(responseHandle);
}
if (blocking == true)
if (blocking is true)
{
response = await responseHandle.GetResponseAsync(cancellationToken);
return GetResult(response);
@@ -83,10 +83,7 @@ builder.Services.AddCosmosActorStateStorage("actor-state-db", "ActorState");
var app = builder.Build();
app.MapOpenApi();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "Agents API");
});
app.UseSwaggerUI(options => options.SwaggerEndpoint("/openapi/v1.json", "Agents API"));
// Configure the HTTP request pipeline.
app.UseExceptionHandler();
@@ -52,7 +52,7 @@ public class ChatClientConnectionInfo
Enum.TryParse(providerValue, ignoreCase: true, out provider);
}
if (endpoint is null && provider != ClientChatProvider.OpenAI || model is null || provider == ClientChatProvider.Unknown)
if ((endpoint is null && provider != ClientChatProvider.OpenAI) || model is null || provider is ClientChatProvider.Unknown)
{
settings = null;
return false;
@@ -36,19 +36,16 @@ public static class ChatClientExtensions
return chatClientBuilder;
}
private static ChatClientBuilder AddOpenAIClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
return builder.AddOpenAIClient(connectionName, settings =>
private static ChatClientBuilder AddOpenAIClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
builder.AddOpenAIClient(connectionName, settings =>
{
settings.Endpoint = connectionInfo.Endpoint;
settings.Key = connectionInfo.AccessKey;
})
.AddChatClient(connectionInfo.SelectedModel);
}
private static ChatClientBuilder AddAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
return builder.Services.AddChatClient(sp =>
private static ChatClientBuilder AddAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
builder.Services.AddChatClient(sp =>
{
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
@@ -56,16 +53,12 @@ public static class ChatClientExtensions
return client.AsIChatClient(connectionInfo.SelectedModel);
});
}
private static ChatClientBuilder AddOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
var httpKey = $"{connectionName}_http";
builder.Services.AddHttpClient(httpKey, c =>
{
c.BaseAddress = connectionInfo.Endpoint;
});
builder.Services.AddHttpClient(httpKey, c => c.BaseAddress = connectionInfo.Endpoint);
return builder.Services.AddChatClient(sp =>
{
@@ -102,19 +95,16 @@ public static class ChatClientExtensions
return chatClientBuilder;
}
private static ChatClientBuilder AddKeyedOpenAIClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
return builder.AddKeyedOpenAIClient(connectionName, settings =>
private static ChatClientBuilder AddKeyedOpenAIClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
builder.AddKeyedOpenAIClient(connectionName, settings =>
{
settings.Endpoint = connectionInfo.Endpoint;
settings.Key = connectionInfo.AccessKey;
})
.AddKeyedChatClient(connectionName, connectionInfo.SelectedModel);
}
private static ChatClientBuilder AddKeyedAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
return builder.Services.AddKeyedChatClient(connectionName, sp =>
private static ChatClientBuilder AddKeyedAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
builder.Services.AddKeyedChatClient(connectionName, sp =>
{
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
@@ -122,16 +112,12 @@ public static class ChatClientExtensions
return client.AsIChatClient(connectionInfo.SelectedModel);
});
}
private static ChatClientBuilder AddKeyedOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
var httpKey = $"{connectionName}_http";
builder.Services.AddHttpClient(httpKey, c =>
{
c.BaseAddress = connectionInfo.Endpoint;
});
builder.Services.AddHttpClient(httpKey, c => c.BaseAddress = connectionInfo.Endpoint);
return builder.Services.AddKeyedChatClient(connectionName, sp =>
{
@@ -19,7 +19,7 @@ internal sealed class A2AActorClient : IActorClient
// because A2A sdk does not provide a client which can handle multiple agents, we need a client per agent
// for this app the convention is "baseUri/<agentname>"
private readonly ConcurrentDictionary<string, (A2AClient, A2ACardResolver)> _clients = new();
private readonly ConcurrentDictionary<string, (A2AClient, A2ACardResolver)> _clients = [];
public A2AActorClient(ILogger logger, Uri baseUri)
{
@@ -35,10 +35,7 @@ internal sealed class A2AActorClient : IActorClient
return a2aCardResolver.GetAgentCardAsync(cancellationToken);
}
public ValueTask<ActorResponseHandle> GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public ValueTask<ActorResponseHandle> GetResponseAsync(ActorId actorId, string messageId, CancellationToken cancellationToken) => throw new NotImplementedException();
public ValueTask<ActorResponseHandle> SendRequestAsync(ActorRequest request, CancellationToken cancellationToken)
{
@@ -51,9 +48,8 @@ internal sealed class A2AActorClient : IActorClient
private (A2AClient, A2ACardResolver) ResolveClient(ActorType agentName)
=> this.ResolveClient(agentName.Name);
private (A2AClient, A2ACardResolver) ResolveClient(string agentName)
{
return this._clients.GetOrAdd(agentName, name =>
private (A2AClient, A2ACardResolver) ResolveClient(string agentName) =>
this._clients.GetOrAdd(agentName, name =>
{
var uri = new Uri($"{this._uri}/{name}/");
var a2aClient = new A2AClient(uri);
@@ -64,7 +60,6 @@ internal sealed class A2AActorClient : IActorClient
this._logger.LogInformation("Built clients for agent {Agent} with baseUri {Uri}", name, uri);
return (a2aClient, a2aCardResolver);
});
}
private sealed class A2AActorResponseHandle : ActorResponseHandle
{
@@ -77,20 +72,11 @@ internal sealed class A2AActorClient : IActorClient
this._request = request;
}
public override ValueTask CancelAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public override ValueTask CancelAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
public override ValueTask<ActorResponse> GetResponseAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public override ValueTask<ActorResponse> GetResponseAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
public override bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response)
{
throw new NotImplementedException();
}
public override bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response) => throw new NotImplementedException();
public override async IAsyncEnumerable<ActorRequestUpdate> WatchUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -31,8 +31,5 @@
private string? requestId;
private bool ShowRequestId => !string.IsNullOrEmpty(requestId);
protected override void OnInitialized()
{
requestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
protected override void OnInitialized() => requestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
@@ -907,7 +907,7 @@
A2A // Agent-to-Agent protocol
}
private class Conversation
private sealed class Conversation
{
public string SessionId { get; set; } = Guid.NewGuid().ToString();
public string AgentName { get; set; } = "";
@@ -944,32 +944,23 @@
// Conversations start fresh on page load
}
private string GetAgentIcon(string agentName)
{
return agentName?.ToLower() switch
{
"pirate" => "🏴‍☠️",
"knights-and-knaves" => "⚔️",
_ => "🤖"
};
}
private string GetAgentIcon(string agentName) => agentName?.ToLower() switch
{
"pirate" => "🏴‍☠️",
"knights-and-knaves" => "⚔️",
_ => "🤖"
};
private string GetAgentDisplayName(string agentName)
{
return agentName?.ToLower() switch
{
"pirate" => "Pirate",
"knights-and-knaves" => "Knights & Knaves",
_ => agentName ?? "Agent"
};
}
private string GetAgentDisplayName(string agentName) => agentName?.ToLower() switch
{
"pirate" => "Pirate",
"knights-and-knaves" => "Knights & Knaves",
_ => agentName ?? "Agent"
};
private void ToggleA2AExpanded()
{
isA2AExpanded = !isA2AExpanded;
}
private void ToggleA2AExpanded() => isA2AExpanded = !isA2AExpanded;
private async Task DiscoverAgentCard()
private async Task DiscoverAgentCard()
{
if (string.IsNullOrEmpty(selectedAgentName) || isDiscoveringCard)
return;
@@ -99,7 +99,7 @@ internal sealed class HttpActorClient(HttpClient httpClient) : IActorClient
public override bool TryGetResponse([NotNullWhen(true)] out ActorResponse? response)
{
response = this._lastResponse;
return response != null;
return response is not null;
}
public override async IAsyncEnumerable<ActorRequestUpdate> WatchUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
@@ -138,7 +138,7 @@ internal sealed class HttpActorClient(HttpClient httpClient) : IActorClient
private static async IAsyncEnumerable<ActorRequestUpdate> EnumerateAsync(HttpResponseMessage responseMessage, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var responseStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var sseParser = SseParser.Create<ActorRequestUpdate?>(responseStream, (eventType, data) =>
var sseParser = SseParser.Create(responseStream, (eventType, data) =>
{
if (eventType != "message")
{
@@ -159,26 +159,13 @@ internal sealed class HttpActorClient(HttpClient httpClient) : IActorClient
}
}
private async Task<ActorResponse> ReadResponseAsync(HttpResponseMessage responseMessage, CancellationToken cancellationToken)
{
var response = await responseMessage.Content.ReadFromJsonAsync<ActorResponse>(AgentRuntimeJsonUtilities.DefaultOptions, cancellationToken).ConfigureAwait(false);
if (response == null)
{
throw new InvalidOperationException($"No response found for actor '{actorId}' with message ID '{messageId}'.");
}
private async Task<ActorResponse> ReadResponseAsync(HttpResponseMessage responseMessage, CancellationToken cancellationToken) =>
await responseMessage.Content.ReadFromJsonAsync<ActorResponse>(AgentRuntimeJsonUtilities.DefaultOptions, cancellationToken).ConfigureAwait(false) ??
throw new InvalidOperationException($"No response found for actor '{actorId}' with message ID '{messageId}'.");
return response;
}
private static bool IsJsonResponse([NotNullWhen(true)] HttpResponseMessage? response) => response?.Content.Headers.ContentType?.MediaType == "application/json";
private static bool IsJsonResponse([NotNullWhen(true)] HttpResponseMessage? response)
{
return response?.Content.Headers.ContentType?.MediaType == "application/json";
}
private static bool IsStreamingResponse([NotNullWhen(true)] HttpResponseMessage? response)
{
return response?.Content.Headers.ContentType?.MediaType == "text/event-stream";
}
private static bool IsStreamingResponse([NotNullWhen(true)] HttpResponseMessage? response) => response?.Content.Headers.ContentType?.MediaType == "text/event-stream";
protected override void Dispose(bool disposing)
{
@@ -24,10 +24,7 @@ Uri a2aAddress = new("http://localhost:5390/a2a");
builder.Services.AddHttpClient<AgentDiscoveryClient>(client => client.BaseAddress = baseAddress);
builder.Services.AddHttpClient<IActorClient, HttpActorClient>(client => client.BaseAddress = baseAddress);
builder.Services.AddSingleton<A2AActorClient>(sp =>
{
return new A2AActorClient(sp.GetRequiredService<ILogger<A2AActorClient>>(), a2aAddress);
});
builder.Services.AddSingleton(sp => new A2AActorClient(sp.GetRequiredService<ILogger<A2AActorClient>>(), a2aAddress));
var app = builder.Build();
@@ -42,7 +42,7 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(SourceName) // Our custom activity source
.AddSource("Microsoft.Extensions.AI.Agents") // Agent Framework telemetry
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
.AddOtlpExporter(options => { options.Endpoint = new Uri(otlpEndpoint); })
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
.Build();
// Setup metrics with resource and instrument name filtering
@@ -52,7 +52,7 @@ using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("Microsoft.Extensions.AI.Agents") // Agent Framework metrics
.AddHttpClientInstrumentation() // HTTP client metrics
.AddRuntimeInstrumentation() // .NET runtime metrics
.AddOtlpExporter(options => { options.Endpoint = new Uri(otlpEndpoint); })
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
.Build();
// Setup structured logging with OpenTelemetry
@@ -62,10 +62,7 @@ serviceCollection.AddLogging(loggingBuilder => loggingBuilder
.AddOpenTelemetry(options =>
{
options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"));
options.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri(otlpEndpoint);
});
options.AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri(otlpEndpoint));
options.IncludeScopes = true;
options.IncludeFormattedMessage = true;
}));
@@ -97,7 +94,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
appLogger.LogInformation("OpenTelemetry Aspire Demo application started");
[Description("Get the weather for a given location.")]
static async Task<string> GetWeather([Description("The location to get the weather for.")] string location)
static async Task<string> GetWeatherAsync([Description("The location to get the weather for.")] string location)
{
await Task.Delay(2000);
return $"The weather in {location} is cloudy with a high of 15°C.";
@@ -109,7 +106,7 @@ using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new
.AsIChatClient() // Converts a native OpenAI SDK ChatClient into a Microsoft.Extensions.AI.IChatClient
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry(loggerFactory: loggerFactory, sourceName: SourceName, (cfg) => { cfg.EnableSensitiveData = true; })
.UseOpenTelemetry(loggerFactory: loggerFactory, sourceName: SourceName, (cfg) => cfg.EnableSensitiveData = true)
.Build();
appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
@@ -117,7 +114,7 @@ appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
using var agent = new ChatClientAgent(instrumentedChatClient,
name: "OpenTelemetryDemoAgent",
instructions: "You are a helpful assistant that provides concise and informative responses.",
tools: [AIFunctionFactory.Create(GetWeather)])
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
.WithOpenTelemetry(loggerFactory, SourceName); // Enable telemetry on the agent
var thread = agent.GetNewThread();
@@ -127,9 +124,10 @@ appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.
// Create a parent span for the entire agent session
using var sessionActivity = activitySource.StartActivity("Agent Session");
var sessionId = thread.ConversationId ?? Guid.NewGuid().ToString();
sessionActivity?.SetTag("agent.name", "OpenTelemetryDemoAgent");
sessionActivity?.SetTag("session.id", sessionId);
sessionActivity?.SetTag("session.start_time", DateTimeOffset.UtcNow.ToString("O"));
sessionActivity?
.SetTag("agent.name", "OpenTelemetryDemoAgent")
.SetTag("session.id", sessionId)
.SetTag("session.start_time", DateTimeOffset.UtcNow.ToString("O"));
appLogger.LogInformation("Starting agent session with ID: {SessionId}", sessionId);
using (appLogger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessionId, ["AgentName"] = "OpenTelemetryDemoAgent" }))
@@ -152,11 +150,12 @@ using (appLogger.BeginScope(new Dictionary<string, object> { ["SessionId"] = ses
// Create a child span for each individual interaction
using var activity = activitySource.StartActivity("Agent Interaction");
activity?.SetTag("user.input", userInput);
activity?.SetTag("agent.name", "OpenTelemetryDemoAgent");
activity?.SetTag("interaction.number", interactionCount);
activity?
.SetTag("user.input", userInput)
.SetTag("agent.name", "OpenTelemetryDemoAgent")
.SetTag("interaction.number", interactionCount);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var stopwatch = Stopwatch.StartNew();
try
{
@@ -197,9 +196,10 @@ using (appLogger.BeginScope(new Dictionary<string, object> { ["SessionId"] = ses
responseTimeHistogram.Record(responseTime,
new KeyValuePair<string, object?>("status", "error"));
activity?.SetTag("response.success", false);
activity?.SetTag("error.message", ex.Message);
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?
.SetTag("response.success", false)
.SetTag("error.message", ex.Message)
.SetStatus(ActivityStatusCode.Error, ex.Message);
appLogger.LogError(ex, "Agent interaction #{InteractionNumber} failed after {ResponseTime:F2} seconds: {ErrorMessage}",
interactionCount, responseTime, ex.Message);
@@ -207,8 +207,9 @@ using (appLogger.BeginScope(new Dictionary<string, object> { ["SessionId"] = ses
}
// Add session summary to the parent span
sessionActivity?.SetTag("session.total_interactions", interactionCount);
sessionActivity?.SetTag("session.end_time", DateTimeOffset.UtcNow.ToString("O"));
sessionActivity?
.SetTag("session.total_interactions", interactionCount)
.SetTag("session.end_time", DateTimeOffset.UtcNow.ToString("O"));
appLogger.LogInformation("Agent session completed. Total interactions: {TotalInteractions}", interactionCount);
} // End of logging scope
@@ -32,7 +32,7 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
AzureAIAgentsPersistent
}
protected IChatClient GetChatClient(ChatClientProviders provider, ChatClientAgentOptions? options = null)
protected static IChatClient GetChatClient(ChatClientProviders provider, ChatClientAgentOptions? options = null)
=> provider switch
{
ChatClientProviders.OpenAIChatCompletion => GetOpenAIChatClient(),
@@ -46,14 +46,6 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
_ => throw new NotSupportedException($"Provider {provider} is not supported.")
};
protected ChatOptions? GetChatOptions(ChatClientProviders? provider)
=> provider switch
{
ChatClientProviders.OpenAIResponses_InMemoryMessageThread => new() { RawRepresentationFactory = static (_) => new ResponseCreationOptions() { StoredOutputEnabled = false } },
ChatClientProviders.OpenAIResponses_ConversationIdThread => new() { RawRepresentationFactory = static (_) => new ResponseCreationOptions() { StoredOutputEnabled = true } },
_ => null
};
/// <summary>
/// For providers that store the agent and the thread on the server side, this will clean and delete
/// any sample agent and thread that was created during this execution.
@@ -65,16 +57,14 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
/// <remarks>
/// Ideally for faster execution and potential cost savings, server-side agents should be reused.
/// </remarks>
protected Task AgentCleanUpAsync(ChatClientProviders provider, AIAgent agent, AgentThread? thread = null, CancellationToken cancellationToken = default)
{
return provider switch
protected static Task AgentCleanUpAsync(ChatClientProviders provider, AIAgent agent, AgentThread? thread = null, CancellationToken cancellationToken = default)
=> provider switch
{
ChatClientProviders.AzureAIAgentsPersistent => AzureAIAgentsPersistentAgentCleanUpAsync(agent, thread, cancellationToken),
ChatClientProviders.OpenAIAssistant => OpenAIAssistantCleanUpAgentAsync(agent, thread, cancellationToken),
// For other remaining provider sample types, no cleanup is needed as they don't offer a server-side agent/thread clean-up API.
_ => Task.CompletedTask
};
}
/// <summary>
/// Creates a server-side agent identifier based on the specified provider and options.
@@ -84,23 +74,21 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The identifier of the created agent, or <see langword="null"/> if the provider does not use server-side agents.</returns>
/// <remarks>Some server-side agent providers require an agent id reference to be created before it can be invoked.</remarks>
protected Task<string?> AgentCreateAsync(ChatClientProviders provider, ChatClientAgentOptions options, CancellationToken cancellationToken = default)
{
return provider switch
protected static Task<string?> AgentCreateAsync(ChatClientProviders provider, ChatClientAgentOptions options, CancellationToken cancellationToken = default)
=> provider switch
{
ChatClientProviders.OpenAIAssistant => OpenAIAssistantCreateAgentAsync(options, cancellationToken),
ChatClientProviders.AzureAIAgentsPersistent => AzureAIAgentsPersistentCreateAgentAsync(options, cancellationToken),
_ => Task.FromResult<string?>(null)
};
}
#region Private GetChatClient
private IChatClient GetOpenAIChatClient()
private static IChatClient GetOpenAIChatClient()
=> new ChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.AsIChatClient();
private IChatClient GetAzureOpenAIChatClient()
private static IChatClient GetAzureOpenAIChatClient()
=> ((TestConfiguration.AzureOpenAI.ApiKey is null)
// Use Azure CLI credentials if API key is not provided.
? new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new AzureCliCredential())
@@ -108,23 +96,23 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
.GetChatClient(TestConfiguration.AzureOpenAI.DeploymentName)
.AsIChatClient();
private IChatClient GetOpenAIResponsesClient()
private static IChatClient GetOpenAIResponsesClient()
=> new OpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.AsIChatClient();
private IChatClient GetAzureAIAgentPersistentClient(ChatClientAgentOptions options)
private static IChatClient GetAzureAIAgentPersistentClient(ChatClientAgentOptions options)
=> new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential()).AsNewIChatClient(options.Id!);
private IChatClient GetOpenAIAssistantChatClient(ChatClientAgentOptions options)
private static IChatClient GetOpenAIAssistantChatClient(ChatClientAgentOptions options)
=> new AssistantClient(TestConfiguration.OpenAI.ApiKey).AsIChatClient(options.Id!);
#endregion
#region Private AgentCreate
private async Task<string?> AzureAIAgentsPersistentCreateAgentAsync(ChatClientAgentOptions options, CancellationToken cancellationToken)
private static async Task<string?> AzureAIAgentsPersistentCreateAgentAsync(ChatClientAgentOptions options, CancellationToken cancellationToken)
{
var persistentAgentsClient = new Azure.AI.Agents.Persistent.PersistentAgentsAdministrationClient(
var persistentAgentsClient = new PersistentAgentsAdministrationClient(
TestConfiguration.AzureAI.Endpoint,
new AzureCliCredential());
@@ -138,9 +126,9 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
return result?.Value.Id;
}
private async Task<string?> OpenAIAssistantCreateAgentAsync(ChatClientAgentOptions options, CancellationToken cancellationToken)
private static async Task<string?> OpenAIAssistantCreateAgentAsync(ChatClientAgentOptions options, CancellationToken cancellationToken)
{
var assistantClient = new OpenAI.Assistants.AssistantClient(TestConfiguration.OpenAI.ApiKey);
var assistantClient = new AssistantClient(TestConfiguration.OpenAI.ApiKey);
Assistant assistant = await assistantClient.CreateAssistantAsync(
TestConfiguration.OpenAI.ChatModelId,
new()
@@ -157,7 +145,7 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
#region Private AgentCleanUp
private async Task AzureAIAgentsPersistentAgentCleanUpAsync(AIAgent agent, AgentThread? thread, CancellationToken cancellationToken)
private static async Task AzureAIAgentsPersistentAgentCleanUpAsync(AIAgent agent, AgentThread? thread, CancellationToken cancellationToken)
{
var persistentAgentsClient = (agent as ChatClientAgent)?.ChatClient.GetService<PersistentAgentsClient>() ??
throw new InvalidOperationException("The provided chat client is not a Persistent Agents Chat Client");
@@ -171,7 +159,7 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
}
}
private async Task OpenAIAssistantCleanUpAgentAsync(AIAgent agent, AgentThread? thread, CancellationToken cancellationToken)
private static async Task OpenAIAssistantCleanUpAgentAsync(AIAgent agent, AgentThread? thread, CancellationToken cancellationToken)
{
var assistantClient = (agent as ChatClientAgent)?.ChatClient
.GetService<AssistantClient>()
@@ -18,11 +18,11 @@ public class ConcurrentOrchestration_Intro(ITestOutputHelper output) : Orchestra
{
// Define the agents
ChatClientAgent physicist =
this.CreateAgent(
CreateAgent(
instructions: "You are an expert in physics. You answer questions from a physics perspective.",
description: "An expert in physics");
ChatClientAgent chemist =
this.CreateAgent(
CreateAgent(
instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective.",
description: "An expert in chemistry");
@@ -36,14 +36,14 @@ public class ConcurrentOrchestration_Intro(ITestOutputHelper output) : Orchestra
new(physicist, chemist)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
ResponseCallback = monitor.ResponseCallbackAsync,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null,
};
// Run the orchestration
string input = "What is temperature?";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
const string Input = "What is temperature?";
Console.WriteLine($"\n# INPUT: {Input}\n");
AgentRunResponse result = await orchestration.RunAsync(Input);
Console.WriteLine($"\n# RESULT:\n{string.Join("\n\n", result.Messages.Select(r => $"{r.Text}"))}");
@@ -19,15 +19,15 @@ public class ConcurrentOrchestration_With_StructuredOutput(ITestOutputHelper out
{
// Define the agents
ChatClientAgent agent1 =
this.CreateAgent(
CreateAgent(
instructions: "You are an expert in identifying themes in articles. Given an article, identify the main themes.",
description: "An expert in identifying themes in articles");
ChatClientAgent agent2 =
this.CreateAgent(
CreateAgent(
instructions: "You are an expert in sentiment analysis. Given an article, identify the sentiment.",
description: "An expert in sentiment analysis");
ChatClientAgent agent3 =
this.CreateAgent(
CreateAgent(
instructions: "You are an expert in entity recognition. Given an article, extract the entities.",
description: "An expert in entity recognition");
@@ -35,11 +35,11 @@ public class ConcurrentOrchestration_With_StructuredOutput(ITestOutputHelper out
ConcurrentOrchestration orchestration = new(agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory };
// Run the orchestration
const string resourceId = "Hamlet_full_play_summary.txt";
string input = Resources.Read(resourceId);
Console.WriteLine($"\n# INPUT: @{resourceId}\n");
const string ResourceId = "Hamlet_full_play_summary.txt";
string input = Resources.Read(ResourceId);
Console.WriteLine($"\n# INPUT: @{ResourceId}\n");
var output = await orchestration.RunAsync<Analysis>(this.CreateChatClient(), input);
var output = await orchestration.RunAsync<Analysis>(CreateChatClient(), input);
Console.WriteLine($"\n# RESULT:\n{JsonSerializer.Serialize(output, s_options)}");
}
@@ -50,5 +50,5 @@ public class ConcurrentOrchestration_With_StructuredOutput(ITestOutputHelper out
public IList<string> Sentiments { get; set; } = [];
public IList<string> Entities { get; set; } = [];
}
#pragma warning restore CA1812 // Avoid uninstantiated internal classes
#pragma warning restore CA1812
}
@@ -24,7 +24,7 @@ public class GroupChatOrchestration_Intro(ITestOutputHelper output) : Orchestrat
{
// Define the agents
ChatClientAgent writer =
this.CreateAgent(
CreateAgent(
name: "CopyWriter",
description: "A copy writer",
instructions:
@@ -37,7 +37,7 @@ public class GroupChatOrchestration_Intro(ITestOutputHelper output) : Orchestrat
Consider suggestions when refining an idea.
""");
ChatClientAgent editor =
this.CreateAgent(
CreateAgent(
name: "Reviewer",
description: "An editor.",
instructions:
@@ -62,13 +62,13 @@ public class GroupChatOrchestration_Intro(ITestOutputHelper output) : Orchestrat
editor)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
ResponseCallback = monitor.ResponseCallbackAsync,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null,
};
string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
const string Input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {Input}\n");
AgentRunResponse result = await orchestration.RunAsync(Input);
Console.WriteLine($"\n# RESULT: {result}");
this.DisplayHistory(monitor.History);
@@ -18,7 +18,7 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
{
// Define the agents
ChatClientAgent farmer =
this.CreateAgent(
CreateAgent(
name: "Farmer",
description: "A rural farmer from Southeast Asia.",
instructions:
@@ -29,7 +29,7 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent developer =
this.CreateAgent(
CreateAgent(
name: "Developer",
description: "An urban software developer from the United States.",
instructions:
@@ -40,7 +40,7 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent teacher =
this.CreateAgent(
CreateAgent(
name: "Teacher",
description: "A retired history teacher from Eastern Europe",
instructions:
@@ -51,7 +51,7 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent activist =
this.CreateAgent(
CreateAgent(
name: "Activist",
description: "A young activist from South America.",
instructions:
@@ -61,7 +61,7 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent spiritual =
this.CreateAgent(
CreateAgent(
name: "SpiritualLeader",
description: "A spiritual leader from the Middle East.",
instructions:
@@ -71,7 +71,7 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent artist =
this.CreateAgent(
CreateAgent(
name: "Artist",
description: "An artist from Africa.",
instructions:
@@ -81,7 +81,7 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent immigrant =
this.CreateAgent(
CreateAgent(
name: "Immigrant",
description: "An immigrant entrepreneur from Asia living in Canada.",
instructions:
@@ -92,7 +92,7 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent doctor =
this.CreateAgent(
CreateAgent(
name: "Doctor",
description: "A doctor from Scandinavia.",
instructions:
@@ -108,12 +108,12 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
OrchestrationMonitor monitor = new();
// Define the orchestration
const string topic = "What does a good life mean to you personally?";
const string Topic = "What does a good life mean to you personally?";
GroupChatOrchestration orchestration =
new(
new AIGroupChatManager(
topic,
this.CreateChatClient())
Topic,
CreateChatClient())
{
MaximumInvocationCount = 5
},
@@ -127,12 +127,12 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
doctor)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
ResponseCallback = monitor.ResponseCallbackAsync,
};
// Run the orchestration
Console.WriteLine($"\n# INPUT: {topic}\n");
AgentRunResponse result = await orchestration.RunAsync(topic);
Console.WriteLine($"\n# INPUT: {Topic}\n");
AgentRunResponse result = await orchestration.RunAsync(Topic);
Console.WriteLine($"\n# RESULT: {result}");
this.DisplayHistory(monitor.History);
@@ -167,21 +167,21 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O
}
/// <inheritdoc/>
protected override ValueTask<GroupChatManagerResult<string>> FilterResults(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
protected override ValueTask<GroupChatManagerResult<string>> FilterResultsAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
this.GetResponseAsync<string>(history, Prompts.Filter(topic), cancellationToken);
/// <inheritdoc/>
protected override ValueTask<GroupChatManagerResult<string>> SelectNextAgent(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default) =>
protected override ValueTask<GroupChatManagerResult<string>> SelectNextAgentAsync(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default) =>
this.GetResponseAsync<string>(history, Prompts.Selection(topic, team.FormatList()), cancellationToken);
/// <inheritdoc/>
protected override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
protected override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInputAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
new(new GroupChatManagerResult<bool>(false) { Reason = "The AI group chat manager does not request user input." });
/// <inheritdoc/>
protected override async ValueTask<GroupChatManagerResult<bool>> ShouldTerminate(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
protected override async ValueTask<GroupChatManagerResult<bool>> ShouldTerminateAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<bool> result = await base.ShouldTerminate(history, cancellationToken);
GroupChatManagerResult<bool> result = await base.ShouldTerminateAsync(history, cancellationToken);
if (!result.Value)
{
result = await this.GetResponseAsync<bool>(history, Prompts.Termination(topic), cancellationToken);
@@ -16,7 +16,7 @@ public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output
{
// Define the agents
ChatClientAgent writer =
this.CreateAgent(
CreateAgent(
name: "CopyWriter",
description: "A copy writer",
instructions:
@@ -29,7 +29,7 @@ public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output
Consider suggestions when refining an idea.
""");
ChatClientAgent editor =
this.CreateAgent(
CreateAgent(
name: "Reviewer",
description: "An editor.",
instructions:
@@ -63,13 +63,13 @@ public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output
editor)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
ResponseCallback = monitor.ResponseCallbackAsync,
};
// Run the orchestration
string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
const string Input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {Input}\n");
AgentRunResponse result = await orchestration.RunAsync(Input);
Console.WriteLine($"\n# RESULT: {result}");
this.DisplayHistory(monitor.History);
@@ -84,7 +84,7 @@ public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output
/// </remarks>
private sealed class CustomRoundRobinGroupChatManager : RoundRobinGroupChatManager
{
protected override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
protected override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInputAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
string? lastAgent = history.LastOrDefault()?.AuthorName;
@@ -20,24 +20,24 @@ public class HandoffOrchestration_Intro(ITestOutputHelper output) : Orchestratio
{
// Define the agents & tools
ChatClientAgent triageAgent =
this.CreateAgent(
CreateAgent(
instructions: "A customer support agent that triages issues.",
name: "TriageAgent",
description: "Handle customer requests.");
ChatClientAgent statusAgent =
this.CreateAgent(
CreateAgent(
name: "OrderStatusAgent",
instructions: "Handle order status requests.",
description: "A customer support agent that checks order status.",
functions: AIFunctionFactory.Create(OrderFunctions.CheckOrderStatus));
ChatClientAgent returnAgent =
this.CreateAgent(
CreateAgent(
name: "OrderReturnAgent",
instructions: "Handle order return requests.",
description: "A customer support agent that handles order returns.",
functions: AIFunctionFactory.Create(OrderFunctions.ProcessReturn));
ChatClientAgent refundAgent =
this.CreateAgent(
CreateAgent(
name: "OrderRefundAgent",
instructions: "Handle order refund requests.",
description: "A customer support agent that handles order refund.",
@@ -49,7 +49,7 @@ public class HandoffOrchestration_Intro(ITestOutputHelper output) : Orchestratio
OrchestrationMonitor monitor = new();
// Define user responses for InteractiveCallback (since sample is not interactive)
Queue<string> responses = new();
string task = "I am a customer that needs help with my orders";
const string Task = "I am a customer that needs help with my orders";
responses.Enqueue("I'd like to track the status of my order");
responses.Enqueue("My order ID is 123");
responses.Enqueue("I want to return another order of mine");
@@ -75,13 +75,13 @@ public class HandoffOrchestration_Intro(ITestOutputHelper output) : Orchestratio
return new(input);
},
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
ResponseCallback = monitor.ResponseCallbackAsync,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null,
};
// Run the orchestration
Console.WriteLine($"\n# INPUT:\n{task}\n");
AgentRunResponse result = await orchestration.RunAsync(task);
Console.WriteLine($"\n# INPUT:\n{Task}\n");
AgentRunResponse result = await orchestration.RunAsync(Task);
Console.WriteLine($"\n# RESULT: {result}");
@@ -22,18 +22,18 @@ public class HandoffOrchestration_With_StructuredInput(ITestOutputHelper output)
// Define the agents
ChatClientAgent triageAgent =
this.CreateAgent(
CreateAgent(
instructions: "Given a GitHub issue, triage it.",
name: "TriageAgent",
description: "An agent that triages GitHub issues");
ChatClientAgent pythonAgent =
this.CreateAgent(
CreateAgent(
instructions: "You are an agent that handles Python related GitHub issues.",
name: "PythonAgent",
description: "An agent that handles Python related issues",
functions: githubAddLabelFunction);
ChatClientAgent dotnetAgent =
this.CreateAgent(
CreateAgent(
instructions: "You are an agent that handles .NET related GitHub issues.",
name: "DotNetAgent",
description: "An agent that handles .NET related issues",
@@ -51,7 +51,7 @@ public class HandoffOrchestration_With_StructuredInput(ITestOutputHelper output)
.Add(triageAgent, [dotnetAgent, pythonAgent]))
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
ResponseCallback = monitor.ResponseCallbackAsync,
};
GithubIssue input =
@@ -105,9 +105,6 @@ public class HandoffOrchestration_With_StructuredInput(ITestOutputHelper output)
{
public Dictionary<string, string[]> Labels { get; } = [];
public void AddLabels(string issueId, params string[] labels)
{
this.Labels[issueId] = labels;
}
public void AddLabels(string issueId, params string[] labels) => this.Labels[issueId] = labels;
}
}
@@ -67,14 +67,14 @@ public class SequentialOrchestration_Foundry_Agents(ITestOutputHelper output) :
new(analystAgent, writerAgent, editorAgent)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
ResponseCallback = monitor.ResponseCallbackAsync,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null,
};
// Run the orchestration
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
const string Input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {Input}\n");
AgentRunResponse result = await orchestration.RunAsync(Input);
Console.WriteLine($"\n# RESULT: {result}");
this.DisplayHistory(monitor.History);
@@ -19,7 +19,7 @@ public class SequentialOrchestration_Intro(ITestOutputHelper output) : Orchestra
{
// Define the agents
ChatClientAgent analystAgent =
this.CreateAgent(
CreateAgent(
name: "Analyst",
instructions:
"""
@@ -30,7 +30,7 @@ public class SequentialOrchestration_Intro(ITestOutputHelper output) : Orchestra
""",
description: "A agent that extracts key concepts from a product description.");
ChatClientAgent writerAgent =
this.CreateAgent(
CreateAgent(
name: "copywriter",
instructions:
"""
@@ -40,7 +40,7 @@ public class SequentialOrchestration_Intro(ITestOutputHelper output) : Orchestra
""",
description: "An agent that writes a marketing copy based on the extracted concepts.");
ChatClientAgent editorAgent =
this.CreateAgent(
CreateAgent(
name: "editor",
instructions:
"""
@@ -58,14 +58,14 @@ public class SequentialOrchestration_Intro(ITestOutputHelper output) : Orchestra
new(analystAgent, writerAgent, editorAgent)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
ResponseCallback = monitor.ResponseCallbackAsync,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null,
};
// Run the orchestration
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
const string Input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {Input}\n");
AgentRunResponse result = await orchestration.RunAsync(Input);
Console.WriteLine($"\n# RESULT: {result}");
this.DisplayHistory(monitor.History);
@@ -63,14 +63,14 @@ public class SequentialOrchestration_Multi_Agent(ITestOutputHelper output) : Orc
new(analystAgent, writerAgent, editorAgent)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
ResponseCallback = monitor.ResponseCallbackAsync,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null,
};
// Run the orchestration
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
const string Input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {Input}\n");
AgentRunResponse result = await orchestration.RunAsync(Input);
Console.WriteLine($"\n# RESULT: {result}");
this.DisplayHistory(monitor.History);
@@ -16,7 +16,7 @@ public class SequentialOrchestration_With_Cancellation(ITestOutputHelper output)
{
// Define the agents
ChatClientAgent agent =
this.CreateAgent(
CreateAgent(
"""
If the input message is a number, return the number incremented by one.
""",
@@ -26,10 +26,10 @@ public class SequentialOrchestration_With_Cancellation(ITestOutputHelper output)
SequentialOrchestration orchestration = new(agent) { LoggerFactory = this.LoggerFactory };
// Run the orchestration
string input = "42";
Console.WriteLine($"\n# INPUT: {input}\n");
const string Input = "42";
Console.WriteLine($"\n# INPUT: {Input}\n");
OrchestratingAgentResponse result = await orchestration.RunAsync([new ChatMessage(ChatRole.User, input)]);
OrchestratingAgentResponse result = await orchestration.RunAsync([new ChatMessage(ChatRole.User, Input)]);
result.Cancel();
await Task.Delay(TimeSpan.FromSeconds(3));
@@ -20,7 +20,7 @@ ChatClientAgentOptions agentOptions = new(name: "HelpfulAssistant", instructions
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
ResponseFormat = ChatResponseFormat.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(PersonInfo)),
schemaName: "PersonInfo",
schemaDescription: "Information about a person including their name, age, and occupation")
@@ -84,15 +84,13 @@ namespace SampleApp
{
this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
if (serializedStoreState.ValueKind == JsonValueKind.String)
if (serializedStoreState.ValueKind is JsonValueKind.String)
{
// Here we can deserialize the thread id so that we can access the same messages as before the suspension.
this._threadId = JsonSerializer.Deserialize<string>(serializedStoreState);
this._threadId = serializedStoreState.Deserialize<string>();
}
}
public string? ThreadId => this._threadId;
public async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken)
{
this._threadId ??= Guid.NewGuid().ToString();
@@ -122,18 +120,15 @@ namespace SampleApp
cancellationToken)
.ToListAsync(cancellationToken);
var messages = records
.Select(x => JsonSerializer.Deserialize<ChatMessage>(x.SerializedMessage!)!)
.ToList();
var messages = records.ConvertAll(x => JsonSerializer.Deserialize<ChatMessage>(x.SerializedMessage!)!)
;
messages.Reverse();
return messages;
}
public ValueTask<JsonElement?> SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
public ValueTask<JsonElement?> SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
// We have to serialize the thread id, so that on deserialization we can retrieve the messages using the same thread id.
return new ValueTask<JsonElement?>(JsonSerializer.SerializeToElement(this._threadId));
}
new(JsonSerializer.SerializeToElement(this._threadId));
/// <summary>
/// The data structure used to store chat history items in the vector store.
@@ -69,7 +69,7 @@ internal sealed class SampleService(AIAgent agent, [FromKeyedServices("AppShutdo
// Delay a little to allow the service to finish starting.
await Task.Delay(100, cancellationToken);
while (cancellationToken.IsCancellationRequested is false)
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("\nAgent: Ask me to tell you a joke about a specific topic. To exit just press Ctrl+C or enter without any input.\n");
Console.Write("> ");
@@ -83,7 +83,7 @@ internal sealed class SampleService(AIAgent agent, [FromKeyedServices("AppShutdo
}
// Stream the output to the console as it is generated.
await foreach (var update in agent.RunStreamingAsync(input, this._thread!, cancellationToken: cancellationToken))
await foreach (var update in agent.RunStreamingAsync(input, this._thread, cancellationToken: cancellationToken))
{
Console.Write(update);
}
@@ -102,7 +102,7 @@ namespace SampleApp
public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
{
this._chatClient = chatClient;
this.UserInfo = JsonSerializer.Deserialize<UserInfo>(serializedState, jsonSerializerOptions) ?? new UserInfo();
this.UserInfo = serializedState.Deserialize<UserInfo>(jsonSerializerOptions) ?? new UserInfo();
}
public UserInfo UserInfo { get; set; }
@@ -110,7 +110,7 @@ namespace SampleApp
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
if ((this.UserInfo.UserName == null || this.UserInfo.UserAge == null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
{
var result = await this._chatClient.GetResponseAsync<UserInfo>(
context.RequestMessages,
@@ -130,15 +130,15 @@ namespace SampleApp
StringBuilder instructions = new();
// If we don't already know the user's name and age, add instructions to ask for them, otherwise just provide what we have to the context.
instructions.AppendLine(
this.UserInfo.UserName == null ?
"Ask the user for their name and politely decline to answer any questions until they provide it." :
$"The user's name is {this.UserInfo.UserName}.");
instructions.AppendLine(
this.UserInfo.UserAge == null ?
"Ask the user for their age and politely decline to answer any questions until they provide it." :
$"The user's age is {this.UserInfo.UserAge}.");
instructions
.AppendLine(
this.UserInfo.UserName is null ?
"Ask the user for their name and politely decline to answer any questions until they provide it." :
$"The user's name is {this.UserInfo.UserName}.")
.AppendLine(
this.UserInfo.UserAge is null ?
"Ask the user for their age and politely decline to answer any questions until they provide it." :
$"The user's age is {this.UserInfo.UserAge}.");
return new ValueTask<AIContext>(new AIContext
{
@@ -153,7 +153,7 @@ namespace SampleApp
public override ValueTask DeserializeAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
this.UserInfo = JsonSerializer.Deserialize<UserInfo>(serializedState, jsonSerializerOptions) ?? new UserInfo();
this.UserInfo = serializedState.Deserialize<UserInfo>(jsonSerializerOptions) ?? new UserInfo();
return default;
}
}
@@ -4,7 +4,6 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
@@ -13,7 +12,6 @@ using System.Threading.Tasks;
using System.Web;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Client;
@@ -30,10 +28,7 @@ using var sharedHandler = new SocketsHttpHandler
};
using var httpClient = new HttpClient(sharedHandler);
var consoleLoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
});
var consoleLoggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
// Create SSE client transport for the MCP server
var serverUrl = "http://localhost:7071/";
@@ -59,7 +54,7 @@ AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You answer questions related to the weather.", tools: [.. mcpTools.Select(mcpTool => (AITool)mcpTool)]);
.CreateAIAgent(instructions: "You answer questions related to the weather.", tools: [.. mcpTools]);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Get current weather alerts for New York?"));
@@ -92,8 +87,8 @@ static async Task<string?> HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri
var code = query["code"];
var error = query["error"];
string responseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>";
byte[] buffer = Encoding.UTF8.GetBytes(responseHtml);
const string ResponseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>";
byte[] buffer = Encoding.UTF8.GetBytes(ResponseHtml);
context.Response.ContentLength64 = buffer.Length;
context.Response.ContentType = "text/html";
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
@@ -44,17 +44,16 @@ public static class Program
var feedbackProvider = new FeedbackExecutor(chatClient);
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(sloganWriter);
builder.AddEdge(sloganWriter, feedbackProvider);
builder.AddEdge(feedbackProvider, sloganWriter);
var workflow = builder.Build<string>();
var workflow = new WorkflowBuilder(sloganWriter)
.AddEdge(sloganWriter, feedbackProvider)
.AddEdge(feedbackProvider, sloganWriter)
.Build<string>();
// Execute the workflow
var ask = "Create a slogan for a new electric SUV that is affordable and fun to drive.";
StreamingRun run = await InProcessExecution.StreamAsync(workflow, ask);
StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Create a slogan for a new electric SUV that is affordable and fun to drive.");
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is SloganGeneratedEvent || evt is FeedbackEvent)
if (evt is SloganGeneratedEvent or FeedbackEvent)
{
// Custom events to allow us to monitor the progress of the workflow.
Console.WriteLine($"{evt}");
@@ -114,10 +113,6 @@ internal sealed class SloganWriterExecutor
IMessageHandler<string, SloganResult>,
IMessageHandler<FeedbackResult, SloganResult>
{
private const string Instruction = """
You are a professional slogan writer. You will be given a task to create a slogan.
""";
private readonly AIAgent _agent;
private readonly AgentThread _thread;
@@ -127,13 +122,11 @@ internal sealed class SloganWriterExecutor
/// <param name="chatClient">The chat client to use for the AI agent.</param>
public SloganWriterExecutor(IChatClient chatClient)
{
var agentOptions = new ChatClientAgentOptions(instructions: Instruction)
ChatClientAgentOptions agentOptions = new(instructions: "You are a professional slogan writer. You will be given a task to create a slogan.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(SloganResult))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(SloganResult)))
}
};
@@ -184,10 +177,6 @@ internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEve
/// </summary>
internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, IMessageHandler<SloganResult>
{
private const string Instruction = """
You are a professional editor. You will be given a slogan and the task it is meant to accomplish.
""";
private readonly AIAgent _agent;
private readonly AgentThread _thread;
@@ -195,7 +184,7 @@ internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, I
public int MaxAttempts { get; init; } = 3;
private int _attempts = 0;
private int _attempts;
/// <summary>
/// Initializes a new instance of the <see cref="FeedbackExecutor"/> class.
@@ -203,13 +192,11 @@ internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, I
/// <param name="chatClient">The chat client to use for the AI agent.</param>
public FeedbackExecutor(IChatClient chatClient)
{
var agentOptions = new ChatClientAgentOptions(instructions: Instruction)
ChatClientAgentOptions agentOptions = new(instructions: "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(FeedbackResult))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(FeedbackResult)))
}
};
@@ -229,11 +216,13 @@ internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, I
var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
await context.AddEventAsync(new FeedbackEvent(feedback));
if (feedback.Rating >= this.MinimumRating)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"The following slogan was accepted:\n\n{message.Slogan}"));
return;
}
if (this._attempts >= this.MaxAttempts)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}"));
@@ -34,10 +34,10 @@ public static class Program
AIAgent englishAgent = await GetTranslationAgentAsync("English", persistentAgentsClient, model);
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(frenchAgent);
builder.AddEdge(frenchAgent, spanishAgent);
builder.AddEdge(spanishAgent, englishAgent);
var workflow = builder.Build<ChatMessage>();
var workflow = new WorkflowBuilder(frenchAgent)
.AddEdge(frenchAgent, spanishAgent)
.AddEdge(spanishAgent, englishAgent)
.Build<ChatMessage>();
// Execute the workflow
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
@@ -71,11 +71,10 @@ public static class Program
PersistentAgentsClient persistentAgentsClient,
string model)
{
string instructions = $"You are a translation assistant that translates the provided text to {targetLanguage}.";
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
model: model,
name: $"{targetLanguage} Translator",
instructions: instructions);
instructions: $"You are a translation assistant that translates the provided text to {targetLanguage}.");
return await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
}
@@ -64,7 +64,7 @@ public static class Program
Dictionary<string, List<AgentRunResponseUpdate>> buffer = [];
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread).ConfigureAwait(false))
{
if (update.MessageId == null)
if (update.MessageId is null)
{
// skip updates that don't have a message ID
continue;
@@ -26,11 +26,10 @@ internal static class WorkflowHelper
AIAgent englishAgent = GetLanguageAgent("English", chatClient);
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(startExecutor);
builder.AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent]);
builder.AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent]);
return builder.Build<List<ChatMessage>>();
return new WorkflowBuilder(startExecutor)
.AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent])
.AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent])
.Build<List<ChatMessage>>();
}
/// <summary>
@@ -39,11 +38,8 @@ internal static class WorkflowHelper
/// <param name="targetLanguage">The target language for translation</param>
/// <param name="chatClient">The chat client to use for the agent</param>
/// <returns>A ChatClientAgent configured for the specified language</returns>
private static ChatClientAgent GetLanguageAgent(string targetLanguage, IChatClient chatClient)
{
string instructions = $"You're a helpful assistant who always responds in {targetLanguage}.";
return new ChatClientAgent(chatClient, instructions, name: $"{targetLanguage}Agent");
}
private static ChatClientAgent GetLanguageAgent(string targetLanguage, IChatClient chatClient) =>
new(chatClient, instructions: $"You're a helpful assistant who always responds in {targetLanguage}.", name: $"{targetLanguage}Agent");
/// <summary>
/// Executor that starts the concurrent processing by sending messages to the agents.
@@ -51,7 +51,7 @@ public static class Program
// Checkpoints are automatically created at the end of each super step when a
// checkpoint manager is provided. You can store the checkpoint info for later use.
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
if (checkpoint != null)
if (checkpoint is not null)
{
checkpoints.Add(checkpoint);
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
@@ -72,9 +72,9 @@ public static class Program
// Rehydrate a new workflow instance from a saved checkpoint and continue execution
var newWorkflow = WorkflowHelper.GetWorkflow();
var checkpointIndex = 5;
Console.WriteLine($"\n\nHydrating a new workflow instance from the {checkpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
const int CheckpointIndex = 5;
Console.WriteLine($"\n\nHydrating a new workflow instance from the {CheckpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
Checkpointed<StreamingRun> newCheckpointedRun = await InProcessExecution
.StreamAsync(newWorkflow, NumberSignal.Init, checkpointManager)
@@ -23,12 +23,10 @@ internal static class WorkflowHelper
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(guessNumberExecutor)
return new WorkflowBuilder(guessNumberExecutor)
.AddEdge(guessNumberExecutor, judgeExecutor)
.AddEdge(judgeExecutor, guessNumberExecutor)
.Build<NumberSignal>();
return workflow;
}
}
@@ -94,19 +92,15 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
}
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
}
}
/// <summary>
@@ -115,7 +109,7 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
private int _tries;
private const string StateKey = "JudgeExecutorState";
/// <summary>
@@ -149,17 +143,13 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, this._tries);
}
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
context.QueueStateUpdateAsync(StateKey, this._tries);
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
}
}
@@ -50,7 +50,7 @@ public static class Program
// Checkpoints are automatically created at the end of each super step when a
// checkpoint manager is provided. You can store the checkpoint info for later use.
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
if (checkpoint != null)
if (checkpoint is not null)
{
checkpoints.Add(checkpoint);
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
@@ -70,9 +70,9 @@ public static class Program
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
// Restoring from a checkpoint and resuming execution
var checkpointIndex = 5;
Console.WriteLine($"\n\nRestoring from the {checkpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
const int CheckpointIndex = 5;
Console.WriteLine($"\n\nRestoring from the {CheckpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
// Note that we are restoring the state directly to the same run instance.
await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
@@ -23,12 +23,10 @@ internal static class WorkflowHelper
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(guessNumberExecutor)
return new WorkflowBuilder(guessNumberExecutor)
.AddEdge(guessNumberExecutor, judgeExecutor)
.AddEdge(judgeExecutor, guessNumberExecutor)
.Build<NumberSignal>();
return workflow;
}
}
@@ -94,19 +92,15 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
}
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
}
}
/// <summary>
@@ -115,7 +109,7 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
private int _tries;
private const string StateKey = "JudgeExecutorState";
/// <summary>
@@ -149,17 +143,13 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, this._tries);
}
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
context.QueueStateUpdateAsync(StateKey, this._tries);
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
}
}
@@ -57,7 +57,7 @@ public static class Program
// Checkpoints are automatically created at the end of each super step when a
// checkpoint manager is provided. You can store the checkpoint info for later use.
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
if (checkpoint != null)
if (checkpoint is not null)
{
checkpoints.Add(checkpoint);
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
@@ -76,9 +76,9 @@ public static class Program
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
// Restoring from a checkpoint and resuming execution
var checkpointIndex = 1;
Console.WriteLine($"\n\nRestoring from the {checkpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
const int CheckpointIndex = 1;
Console.WriteLine($"\n\nRestoring from the {CheckpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
// Note that we are restoring the state directly to the same run instance.
await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
@@ -109,13 +109,13 @@ public static class Program
{
case NumberSignal.Init:
int initialGuess = ReadIntegerFromConsole("Please provide your initial guess: ");
return request.CreateResponse<int>(initialGuess);
return request.CreateResponse(initialGuess);
case NumberSignal.Above:
int lowerGuess = ReadIntegerFromConsole($"You previously guessed {signal.Number} too large. Please provide a new guess: ");
return request.CreateResponse<int>(lowerGuess);
return request.CreateResponse(lowerGuess);
case NumberSignal.Below:
int higherGuess = ReadIntegerFromConsole($"You previously guessed {signal.Number} too small. Please provide a new guess: ");
return request.CreateResponse<int>(higherGuess);
return request.CreateResponse(higherGuess);
}
}
@@ -20,12 +20,10 @@ internal static class WorkflowHelper
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(numberInputPort)
return new WorkflowBuilder(numberInputPort)
.AddEdge(numberInputPort, judgeExecutor)
.AddEdge(judgeExecutor, numberInputPort)
.Build<SignalWithNumber>();
return workflow;
}
}
@@ -60,7 +58,7 @@ internal sealed class SignalWithNumber
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
private int _tries;
private const string StateKey = "JudgeExecutorState";
/// <summary>
@@ -94,17 +92,13 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, this._tries);
}
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
context.QueueStateUpdateAsync(StateKey, this._tries);
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
}
}
@@ -56,10 +56,10 @@ public static class Program
var aggregationExecutor = new ConcurrentAggregationExecutor();
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(startExecutor);
builder.AddFanOutEdge(startExecutor, targets: [physicist, chemist]);
builder.AddFanInEdge(aggregationExecutor, sources: [physicist, chemist]);
var workflow = builder.Build<string>();
var workflow = new WorkflowBuilder(startExecutor)
.AddFanOutEdge(startExecutor, targets: [physicist, chemist])
.AddFanInEdge(aggregationExecutor, sources: [physicist, chemist])
.Build<string>();
// Execute the workflow in streaming mode
StreamingRun run = await InProcessExecution.StreamAsync(workflow, "What is temperature?");
@@ -53,11 +53,11 @@ public static class Program
var handleSpamExecutor = new HandleSpamExecutor();
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(spamDetectionExecutor);
builder.AddEdge(spamDetectionExecutor, emailAssistantExecutor, condition: GetCondition(expectedResult: false));
builder.AddEdge(emailAssistantExecutor, sendEmailExecutor);
builder.AddEdge(spamDetectionExecutor, handleSpamExecutor, condition: GetCondition(expectedResult: true));
var workflow = builder.Build<ChatMessage>();
var workflow = new WorkflowBuilder(spamDetectionExecutor)
.AddEdge(spamDetectionExecutor, emailAssistantExecutor, condition: GetCondition(expectedResult: false))
.AddEdge(emailAssistantExecutor, sendEmailExecutor)
.AddEdge(spamDetectionExecutor, handleSpamExecutor, condition: GetCondition(expectedResult: true))
.Build<ChatMessage>();
// Read a email from a text file
string email = Resources.Read("spam.txt");
@@ -79,53 +79,34 @@ public static class Program
/// </summary>
/// <param name="expectedResult">The expected spam detection result</param>
/// <returns>A function that evaluates whether a message meets the expected result</returns>
private static Func<object?, bool> GetCondition(bool expectedResult)
{
return detectionResult =>
{
return detectionResult is DetectionResult result && result.IsSpam == expectedResult;
};
}
private static Func<object?, bool> GetCondition(bool expectedResult) =>
detectionResult => detectionResult is DetectionResult result && result.IsSpam == expectedResult;
/// <summary>
/// Creates a spam detection agent.
/// </summary>
/// <returns>A ChatClientAgent configured for spam detection</returns>
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient)
{
string instructions = "You are a spam detection assistant that identifies spam emails.";
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(DetectionResult))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(DetectionResult)))
}
};
return new ChatClientAgent(chatClient, agentOptions);
}
});
/// <summary>
/// Creates an email assistant agent.
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient)
{
string instructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse)))
}
};
return new ChatClientAgent(chatClient, agentOptions);
}
});
}
/// <summary>
@@ -188,7 +169,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
EmailId = Guid.NewGuid().ToString(),
EmailContent = message.Text
};
await context.QueueStateUpdateAsync<Email>(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
// Invoke the agent
var response = await this._spamDetectionAgent.RunAsync(message);
@@ -252,10 +233,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
/// <summary>
/// Simulate the sending of an email.
/// </summary>
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context)
{
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
await context.AddEventAsync(new WorkflowCompletedEvent($"Email sent: {message.Response}"));
}
}
/// <summary>
@@ -94,53 +94,33 @@ public static class Program
/// </summary>
/// <param name="expectedDecision">The expected spam detection decision</param>
/// <returns>A function that evaluates whether a message meets the expected result</returns>
private static Func<object?, bool> GetCondition(SpamDecision expectedDecision)
{
return detectionResult =>
{
return detectionResult is DetectionResult result && result.spamDecision == expectedDecision;
};
}
private static Func<object?, bool> GetCondition(SpamDecision expectedDecision) => detectionResult => detectionResult is DetectionResult result && result.spamDecision == expectedDecision;
/// <summary>
/// Creates a spam detection agent.
/// </summary>
/// <returns>A ChatClientAgent configured for spam detection</returns>
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient)
{
string instructions = "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.";
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(DetectionResult))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(DetectionResult)))
}
};
return new ChatClientAgent(chatClient, agentOptions);
}
});
/// <summary>
/// Creates an email assistant agent.
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient)
{
string instructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse)))
}
};
return new ChatClientAgent(chatClient, agentOptions);
}
});
}
/// <summary>
@@ -213,7 +193,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
EmailId = Guid.NewGuid().ToString(),
EmailContent = message.Text
};
await context.QueueStateUpdateAsync<Email>(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
// Invoke the agent
var response = await this._spamDetectionAgent.RunAsync(message);
@@ -276,10 +256,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
/// <summary>
/// Simulate the sending of an email.
/// </summary>
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context)
{
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
await context.AddEventAsync(new WorkflowCompletedEvent($"Email sent: {message.Response}"));
}
}
/// <summary>
@@ -78,7 +78,7 @@ public static class Program
.AddEdge<AnalysisResult>(
emailAnalysisExecutor,
databaseAccessExecutor,
condition: analysisResult => analysisResult is not null && analysisResult.EmailLength <= LongEmailThreshold)
condition: analysisResult => analysisResult?.EmailLength <= LongEmailThreshold)
// Save the analysis result to the database with summary
.AddEdge(emailSummaryExecutor, databaseAccessExecutor);
var workflow = builder.Build<ChatMessage>();
@@ -141,61 +141,40 @@ public static class Program
/// Create an email analysis agent.
/// </summary>
/// <returns>A ChatClientAgent configured for email analysis</returns>
private static ChatClientAgent GetEmailAnalysisAgent(IChatClient chatClient)
{
string instructions = "You are a spam detection assistant that identifies spam emails.";
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
private static ChatClientAgent GetEmailAnalysisAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(AnalysisResult))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(AnalysisResult)))
}
};
return new ChatClientAgent(chatClient, agentOptions);
}
});
/// <summary>
/// Creates an email assistant agent.
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient)
{
string instructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse)))
}
};
return new ChatClientAgent(chatClient, agentOptions);
}
});
/// <summary>
/// Creates an agent that summarizes emails.
/// </summary>
/// <returns>A ChatClientAgent configured for email summarization</returns>
private static ChatClientAgent GetEmailSummaryAgent(IChatClient chatClient)
{
string instructions = "You are an assistant that helps users summarize emails.";
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
private static ChatClientAgent GetEmailSummaryAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are an assistant that helps users summarize emails.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(EmailSummary))
)
ResponseFormat = ChatResponseFormat.ForJsonSchema(AIJsonUtilities.CreateJsonSchema(typeof(EmailSummary)))
}
};
return new ChatClientAgent(chatClient, agentOptions);
}
});
}
internal static class EmailStateConstants
@@ -271,7 +250,7 @@ internal sealed class EmailAnalysisExecutor : ReflectingExecutor<EmailAnalysisEx
EmailId = Guid.NewGuid().ToString(),
EmailContent = message.Text
};
await context.QueueStateUpdateAsync<Email>(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
// Invoke the agent
var response = await this._emailAnalysisAgent.RunAsync(message);
@@ -335,10 +314,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
/// <summary>
/// Simulate the sending of an email.
/// </summary>
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context)
{
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
await context.AddEventAsync(new WorkflowCompletedEvent($"Email sent: {message.Response}"));
}
}
/// <summary>
@@ -437,7 +414,7 @@ internal sealed class DatabaseAccessExecutor() : ReflectingExecutor<DatabaseAcce
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
{
// 1. Save the email content
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
await Task.Delay(100); // Simulate database access delay
// 2. Save the analysis result
@@ -99,8 +99,8 @@ internal sealed class Program
{
Configuration = this.Configuration
};
Workflow<string> workflow = DeclarativeWorkflowBuilder.Build<string>(this.WorkflowFile, options);
return workflow;
return DeclarativeWorkflowBuilder.Build<string>(this.WorkflowFile, options);
}
private const string DefaultWorkflow = "HelloWorld.yaml";
@@ -164,7 +164,7 @@ internal sealed class Program
Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
if (response is not null)
{
ExternalResponse requestResponse = requestInfo.Request.CreateResponse<InputResponse>(response);
ExternalResponse requestResponse = requestInfo.Request.CreateResponse(response);
await run.Run.SendResponseAsync(requestResponse).ConfigureAwait(false);
response = null;
}
@@ -257,7 +257,7 @@ internal sealed class Program
private static InputResponse HandleExternalRequest(ExternalRequest request)
{
InputRequest? message = request.Data.As<InputRequest>();
string? userInput = null;
string? userInput;
do
{
Console.ForegroundColor = ConsoleColor.DarkGreen;
@@ -52,18 +52,17 @@ public static class Program
{
if (request.DataIs<NumberSignal>())
{
var signal = request.DataAs<NumberSignal>();
switch (signal)
switch (request.DataAs<NumberSignal>())
{
case NumberSignal.Init:
int initialGuess = ReadIntegerFromConsole("Please provide your initial guess: ");
return request.CreateResponse<int>(initialGuess);
return request.CreateResponse(initialGuess);
case NumberSignal.Above:
int lowerGuess = ReadIntegerFromConsole("You previously guessed too large. Please provide a new guess: ");
return request.CreateResponse<int>(lowerGuess);
return request.CreateResponse(lowerGuess);
case NumberSignal.Below:
int higherGuess = ReadIntegerFromConsole("You previously guessed too small. Please provide a new guess: ");
return request.CreateResponse<int>(higherGuess);
return request.CreateResponse(higherGuess);
}
}
@@ -19,12 +19,10 @@ internal static class WorkflowHelper
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(numberInputPort)
return new WorkflowBuilder(numberInputPort)
.AddEdge(numberInputPort, judgeExecutor)
.AddEdge(judgeExecutor, numberInputPort)
.Build<NumberSignal>();
return workflow;
}
}
@@ -44,7 +42,7 @@ internal enum NumberSignal
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
private int _tries;
/// <summary>
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
@@ -108,7 +108,7 @@ internal sealed class GuessNumberExecutor : ReflectingExecutor<GuessNumberExecut
internal sealed class JudgeExecutor : ReflectingExecutor<JudgeExecutor>, IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
private int _tries;
/// <summary>
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
@@ -30,10 +30,10 @@ public static class Program
var aggregate = new AggregationExecutor();
// Build the workflow by connecting executors sequentially
WorkflowBuilder builder = new(fileRead);
builder.AddFanOutEdge(fileRead, targets: [wordCount, paragraphCount]);
builder.AddFanInEdge(aggregate, sources: [wordCount, paragraphCount]);
var workflow = builder.Build<string>();
var workflow = new WorkflowBuilder(fileRead)
.AddFanOutEdge(fileRead, targets: [wordCount, paragraphCount])
.AddFanInEdge(aggregate, sources: [wordCount, paragraphCount])
.Build<string>();
// Execute the workflow with input data
Run run = await InProcessExecution.RunAsync(workflow, "Lorem_Ipsum.txt");
@@ -63,7 +63,7 @@ internal sealed class FileReadExecutor() : ReflectingExecutor<FileReadExecutor>(
string fileContent = Resources.Read(message);
// Store file content in a shared state for access by other executors
string fileID = Guid.NewGuid().ToString();
await context.QueueStateUpdateAsync<string>(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope);
await context.QueueStateUpdateAsync(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope);
return fileID;
}
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
@@ -54,13 +55,8 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor
/// <param name="message">The input text to convert</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <returns>The input text converted to uppercase</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
{
string result = message.ToUpperInvariant();
// The return value will be sent as a message along an edge to subsequent executors
return result;
}
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) =>
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
}
/// <summary>
@@ -76,9 +72,7 @@ internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExec
/// <returns>The input text reversed</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
{
char[] charArray = message.ToCharArray();
System.Array.Reverse(charArray);
string result = new(charArray);
string result = string.Concat(message.Reverse());
// Signal that the workflow is complete
await context.AddEventAsync(new WorkflowCompletedEvent(result)).ConfigureAwait(false);
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
@@ -53,13 +54,8 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor
/// <param name="message">The input text to convert</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <returns>The input text converted to uppercase</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
{
string result = message.ToUpperInvariant();
// The return value will be sent as a message along an edge to subsequent executors
return result;
}
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) =>
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
}
/// <summary>
@@ -75,9 +71,7 @@ internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExec
/// <returns>The input text reversed</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
{
char[] charArray = message.ToCharArray();
System.Array.Reverse(charArray);
string result = new(charArray);
string result = string.Concat(message.Reverse());
// Signal that the workflow is complete
await context.AddEventAsync(new WorkflowCompletedEvent(result)).ConfigureAwait(false);
@@ -40,13 +40,14 @@ public static class Program
AIAgent englishAgent = GetTranslationAgent("English", chatClient);
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(frenchAgent);
builder.AddEdge(frenchAgent, spanishAgent);
builder.AddEdge(spanishAgent, englishAgent);
var workflow = builder.Build<ChatMessage>();
var workflow = new WorkflowBuilder(frenchAgent)
.AddEdge(frenchAgent, spanishAgent)
.AddEdge(spanishAgent, englishAgent)
.Build<ChatMessage>();
// Execute the workflow
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
// Must send the turn token to trigger the agents.
// The agents are wrapped as executors. When they receive messages,
// they will cache the messages and only start processing when they receive a TurnToken.
@@ -66,9 +67,6 @@ public static class Program
/// <param name="targetLanguage">The target language for translation</param>
/// <param name="chatClient">The chat client to use for the agent</param>
/// <returns>A ChatClientAgent configured for the specified language</returns>
private static ChatClientAgent GetTranslationAgent(string targetLanguage, IChatClient chatClient)
{
string instructions = $"You are a translation assistant that translates the provided text to {targetLanguage}.";
return new ChatClientAgent(chatClient, instructions);
}
private static ChatClientAgent GetTranslationAgent(string targetLanguage, IChatClient chatClient) =>
new(chatClient, $"You are a translation assistant that translates the provided text to {targetLanguage}.");
}
@@ -14,10 +14,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -47,7 +47,7 @@ async Task SKAgent()
await azureAgentClient.Administration.DeleteAgentAsync(agent.Id);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -21,10 +21,10 @@ Console.WriteLine($"User Input: {userInput}");
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -59,7 +59,7 @@ async Task SKAgent()
await azureAgentClient.Administration.DeleteAgentAsync(agent.Id);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -16,10 +16,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -61,7 +61,7 @@ async Task SKAgent()
await agent.Client.Administration.DeleteAgentAsync(agent.Id);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -71,12 +71,10 @@ async Task AFAgent()
{
var azureAgentClient = sp.GetRequiredService<PersistentAgentsClient>();
var aiAgent = azureAgentClient.CreateAIAgent(
return azureAgentClient.CreateAIAgent(
deploymentName,
name: "GenerateStory",
instructions: "You are good at telling jokes.");
return aiAgent;
});
await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
@@ -17,10 +17,10 @@ var userInput = "Create a python code file using the code interpreter tool with
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -65,7 +65,7 @@ async Task SKAgent()
await azureAgentClient.Administration.DeleteAgentAsync(agent.Id);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -15,10 +15,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -46,7 +46,7 @@ async Task SKAgent()
}
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -42,10 +42,10 @@ var userInput =
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -111,7 +111,7 @@ async Task SKAgent()
}
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -21,10 +21,10 @@ Console.WriteLine($"User Input: {userInput}");
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
OpenAIResponseAgent agent = new(new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName));
@@ -43,7 +43,7 @@ async Task SKAgent()
}
}
async Task AFAgent()
async Task AFAgentAsync()
{
var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
@@ -16,10 +16,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -39,7 +39,7 @@ async Task SKAgent()
Console.WriteLine(result.Message);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -12,10 +12,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -44,7 +44,7 @@ async Task SKAgent()
}
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -17,10 +17,10 @@ Console.WriteLine($"User Input: {userInput}");
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
var builder = Kernel.CreateBuilder().AddOpenAIChatClient(modelId, apiKey);
@@ -40,7 +40,7 @@ async Task SKAgent()
Console.WriteLine(result.Message);
}
async Task AFAgent()
async Task AFAgentAsync()
{
var agent = new OpenAIClient(apiKey).GetChatClient(modelId).CreateAIAgent(
instructions: "You are a helpful assistant",
@@ -13,10 +13,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -36,7 +36,7 @@ async Task SKAgent()
Console.WriteLine(result.Message);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -3,7 +3,6 @@
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using OpenAI;
@@ -15,15 +14,13 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
var builder = Kernel.CreateBuilder().AddOpenAIChatClient(modelId, apiKey);
var assistantsClient = new AssistantClient(apiKey);
// Define the assistant
@@ -53,7 +50,7 @@ async Task SKAgent()
await assistantsClient.DeleteAssistantAsync(agent.Id);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -22,10 +22,10 @@ static string GetWeather([Description("The location to get the weather for.")] s
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -67,7 +67,7 @@ async Task SKAgent()
await assistantsClient.DeleteAssistantAsync(agent.Id);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -16,10 +16,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -60,7 +60,7 @@ async Task SKAgent()
await assistantsClient.DeleteAssistantAsync(agent.Id);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -70,9 +70,7 @@ async Task AFAgent()
{
var assistantClient = sp.GetRequiredService<AssistantClient>();
var agent = assistantClient.CreateAIAgent(modelId, name: "Joker", instructions: "You are good at telling jokes.");
return agent;
return assistantClient.CreateAIAgent(modelId, name: "Joker", instructions: "You are good at telling jokes.");
});
await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
@@ -19,15 +19,13 @@ var assistantsClient = new AssistantClient(apiKey);
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
var builder = Kernel.CreateBuilder().AddOpenAIChatClient(modelId, apiKey);
// Define the assistant
Assistant assistant = await assistantsClient.CreateAssistantAsync(modelId, enableCodeInterpreter: true);
@@ -71,7 +69,7 @@ async Task SKAgent()
await assistantsClient.DeleteAssistantAsync(agent.Id);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -13,10 +13,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -43,7 +43,7 @@ async Task SKAgent()
}
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -40,10 +40,10 @@ var userInput =
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -108,7 +108,7 @@ async Task SKAgent()
}
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -19,10 +19,10 @@ Console.WriteLine($"User Input: {userInput}");
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
var builder = Kernel.CreateBuilder().AddOpenAIChatClient(modelId, apiKey);
@@ -42,7 +42,7 @@ async Task SKAgent()
}
}
async Task AFAgent()
async Task AFAgentAsync()
{
var agent = new OpenAIClient(apiKey).GetOpenAIResponseClient(modelId).CreateAIAgent(
instructions: "You are a helpful assistant",
@@ -14,10 +14,10 @@ var userInput = "Tell me a joke about a pirate.";
Console.WriteLine($"User Input: {userInput}");
await SKAgent();
await AFAgent();
await SKAgentAsync();
await AFAgentAsync();
async Task SKAgent()
async Task SKAgentAsync()
{
Console.WriteLine("\n=== SK Agent ===\n");
@@ -36,7 +36,7 @@ async Task SKAgent()
Console.WriteLine(result.Message);
}
async Task AFAgent()
async Task AFAgentAsync()
{
Console.WriteLine("\n=== AF Agent ===\n");
@@ -4,7 +4,7 @@ namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Indicates that certain members on a specified <see cref="Type"/> are accessed dynamically,
/// for example through <see cref="System.Reflection"/>.
/// for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which members are being accessed during the execution
@@ -4,7 +4,7 @@ namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Indicates that the specified method requires dynamic access to code that is not referenced
/// statically, for example through <see cref="System.Reflection"/>.
/// statically, for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which methods are unsafe to call when removing unreferenced
@@ -83,12 +83,12 @@ public partial class ConcurrentOrchestration : OrchestratingAgent
tasks.Add(Task.Run(async () =>
{
AIAgent agent = this.Agents[localI];
this.LogOrchestrationSubagentRunning(context, agent);
LogOrchestrationSubagentRunning(context, agent);
completed[localI] = await RunAsync(agent, context, input, options: null, cancellationToken).ConfigureAwait(false);
this.LogOrchestrationSubagentCompleted(context, agent);
await this.CheckpointAsync(input, completed, context, cancellationToken).ConfigureAwait(false);
LogOrchestrationSubagentCompleted(context, agent);
await CheckpointAsync(input, completed, context, cancellationToken).ConfigureAwait(false);
}, cancellationToken));
}
}
@@ -103,8 +103,8 @@ public partial class ConcurrentOrchestration : OrchestratingAgent
return await this.AggregationFunc(completed!, cancellationToken).ConfigureAwait(false);
}
private Task CheckpointAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunResponse?[] completed, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
context.Runtime is not null ? base.WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(messages, completed), OrchestrationJsonContext.Default.ConcurrentState), context, cancellationToken) :
private static Task CheckpointAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunResponse?[] completed, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
context.Runtime is not null ? WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(messages, completed), OrchestrationJsonContext.Default.ConcurrentState), context, cancellationToken) :
Task.CompletedTask;
internal sealed record ConcurrentState(IReadOnlyCollection<ChatMessage> Messages, AgentRunResponse?[] Completed);
@@ -59,7 +59,7 @@ public abstract class GroupChatManager
/// <param name="history">The chat history to filter.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> containing the filtered result as a string.</returns>
protected internal abstract ValueTask<GroupChatManagerResult<string>> FilterResults(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default);
protected internal abstract ValueTask<GroupChatManagerResult<string>> FilterResultsAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default);
/// <summary>
/// Selects the next agent to participate in the group chat based on the provided chat history and team.
@@ -68,7 +68,7 @@ public abstract class GroupChatManager
/// <param name="team">The group of agents participating in the chat.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> containing the identifier of the next agent as a string.</returns>
protected internal abstract ValueTask<GroupChatManagerResult<string>> SelectNextAgent(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default);
protected internal abstract ValueTask<GroupChatManagerResult<string>> SelectNextAgentAsync(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether user input should be requested based on the provided chat history.
@@ -76,7 +76,7 @@ public abstract class GroupChatManager
/// <param name="history">The chat history to consider.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> indicating whether user input should be requested.</returns>
protected internal abstract ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default);
protected internal abstract ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInputAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether the group chat should be terminated based on the provided chat history and invocation count.
@@ -84,7 +84,7 @@ public abstract class GroupChatManager
/// <param name="history">The chat history to consider.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> indicating whether the chat should be terminated.</returns>
protected internal virtual ValueTask<GroupChatManagerResult<bool>> ShouldTerminate(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
protected internal virtual ValueTask<GroupChatManagerResult<bool>> ShouldTerminateAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
bool resultValue = false;
string reason = "Maximum number of invocations has not been reached.";
@@ -76,54 +76,51 @@ public sealed partial class GroupChatOrchestration : OrchestratingAgent
// First, check if we should request user input.
if (interactiveCallback is not null)
{
var userInputResult = await this._manager.ShouldRequestUserInput(allMessages, cancellationToken).ConfigureAwait(false);
if (userInputResult.Value)
var userInputResult = await this._manager.ShouldRequestUserInputAsync(allMessages, cancellationToken).ConfigureAwait(false);
if (userInputResult.Value && interactiveCallback is not null)
{
if (interactiveCallback is not null)
ChatMessage userMessage = await interactiveCallback().ConfigureAwait(false);
allMessages.Add(userMessage);
// Broadcast the user input
if (this.ResponseCallback is not null)
{
ChatMessage userMessage = await interactiveCallback().ConfigureAwait(false);
allMessages.Add(userMessage);
// Broadcast the user input
if (this.ResponseCallback is not null)
{
await this.ResponseCallback([userMessage]).ConfigureAwait(false);
}
await this.CheckpointAsync(allMessages, originalMessageCount, context, cancellationToken).ConfigureAwait(false);
continue;
await this.ResponseCallback([userMessage]).ConfigureAwait(false);
}
await CheckpointAsync(allMessages, originalMessageCount, context, cancellationToken).ConfigureAwait(false);
continue;
}
}
// Check if we should terminate the conversation
var terminateResult = await this._manager.ShouldTerminate(allMessages, cancellationToken).ConfigureAwait(false);
var terminateResult = await this._manager.ShouldTerminateAsync(allMessages, cancellationToken).ConfigureAwait(false);
if (terminateResult.Value)
{
// Filter and return final results
var filterResult = await this._manager.FilterResults(allMessages, cancellationToken).ConfigureAwait(false);
var filterResult = await this._manager.FilterResultsAsync(allMessages, cancellationToken).ConfigureAwait(false);
return new AgentRunResponse([new ChatMessage(ChatRole.Assistant, filterResult.Value) { AuthorName = this.DisplayName }]);
}
// Select the next agent to speak
var nextAgentResult = await this._manager.SelectNextAgent(allMessages, team, cancellationToken).ConfigureAwait(false);
var nextAgentResult = await this._manager.SelectNextAgentAsync(allMessages, team, cancellationToken).ConfigureAwait(false);
AIAgent nextAgent = this.FindAgentByName(nextAgentResult.Value) ??
throw new InvalidOperationException($"AIAgent '{nextAgentResult.Value}' not found in the orchestration.");
// Run the selected agent with all messages.
this.LogOrchestrationSubagentRunning(context, nextAgent);
LogOrchestrationSubagentRunning(context, nextAgent);
AgentRunResponse response = await RunAsync(nextAgent, context, allMessages, options: null, cancellationToken).ConfigureAwait(false);
allMessages.AddRange(response.Messages); // Add the agent's response to the conversation.
this.LogOrchestrationSubagentCompleted(context, nextAgent);
LogOrchestrationSubagentCompleted(context, nextAgent);
await this.CheckpointAsync(allMessages, originalMessageCount, context, cancellationToken).ConfigureAwait(false);
await CheckpointAsync(allMessages, originalMessageCount, context, cancellationToken).ConfigureAwait(false);
}
}
private AIAgent? FindAgentByName(string name) => this.Agents.FirstOrDefault(a => a.DisplayName == name);
private Task CheckpointAsync(List<ChatMessage> allMessages, int originalMessageCount, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
context.Runtime is not null ? base.WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(allMessages, originalMessageCount), OrchestrationJsonContext.Default.GroupChatState), context, cancellationToken) :
private static Task CheckpointAsync(List<ChatMessage> allMessages, int originalMessageCount, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
context.Runtime is not null ? WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(allMessages, originalMessageCount), OrchestrationJsonContext.Default.GroupChatState), context, cancellationToken) :
Task.CompletedTask;
internal sealed record GroupChatState(List<ChatMessage> AllMessages, int OriginalMessageCount);
@@ -19,7 +19,7 @@ public class RoundRobinGroupChatManager : GroupChatManager
private int _currentAgentIndex;
/// <inheritdoc/>
protected internal override ValueTask<GroupChatManagerResult<string>> FilterResults(
protected internal override ValueTask<GroupChatManagerResult<string>> FilterResultsAsync(
IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<string> result = new(history.LastOrDefault()?.Text ?? string.Empty) { Reason = "Default result filter provides the final chat message." };
@@ -27,7 +27,7 @@ public class RoundRobinGroupChatManager : GroupChatManager
}
/// <inheritdoc/>
protected internal override ValueTask<GroupChatManagerResult<string>> SelectNextAgent(
protected internal override ValueTask<GroupChatManagerResult<string>> SelectNextAgentAsync(
IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default)
{
string nextAgent = team.Skip(this._currentAgentIndex).First().Key;
@@ -37,7 +37,7 @@ public class RoundRobinGroupChatManager : GroupChatManager
}
/// <inheritdoc/>
protected internal override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(
protected internal override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInputAsync(
IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<bool> result = new(false) { Reason = "The default round-robin group chat manager does not request user input." };
@@ -85,13 +85,13 @@ public sealed partial class HandoffOrchestration : OrchestratingAgent
while (agent is not null)
{
this.LogOrchestrationSubagentRunning(context, agent);
LogOrchestrationSubagentRunning(context, agent);
if (!this._handoffs.Targets.TryGetValue(agent, out var handoffs) || handoffs.Count == 0)
{
// If no handoff is available, we can run the agent directly and return its response.
response = await RunAsync(agent, context, allMessages, context.Options, cancellationToken).ConfigureAwait(false);
this.LogOrchestrationSubagentCompleted(context, agent);
LogOrchestrationSubagentCompleted(context, agent);
allMessages.AddRange(response.Messages);
agent = null;
await CheckpointAsync().ConfigureAwait(false);
@@ -100,7 +100,7 @@ public sealed partial class HandoffOrchestration : OrchestratingAgent
// Create the options for the next agent request, including handoff functions.
HandoffContext handoffCtx = new(handoffs);
ChatClientAgentRunOptions? options = null;
ChatClientAgentRunOptions? options;
List<AITool> handoffTools = handoffCtx.CreateHandoffFunctions(this.InteractiveCallback is not null);
if (context.Options is ChatClientAgentRunOptions contextOptions)
{
@@ -115,7 +115,7 @@ public sealed partial class HandoffOrchestration : OrchestratingAgent
// Invoke the next agent with all of the messages collected so far.
response = await RunAsync(agent, context, allMessages, options, cancellationToken).ConfigureAwait(false);
this.LogOrchestrationSubagentCompleted(context, agent);
LogOrchestrationSubagentCompleted(context, agent);
allMessages.AddRange(response.Messages);
agent = handoffCtx.TargetedAgent;
RemoveHandoffFunctionCalls(response, handoffTools);
@@ -139,7 +139,7 @@ public sealed partial class HandoffOrchestration : OrchestratingAgent
return response;
Task CheckpointAsync() => context.Runtime is not null ?
base.WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(agent?.Id, allMessages, originalMessageCount), OrchestrationJsonContext.Default.HandoffState), context, cancellationToken) :
WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(agent?.Id, allMessages, originalMessageCount), OrchestrationJsonContext.Default.HandoffState), context, cancellationToken) :
Task.CompletedTask;
}
@@ -17,7 +17,7 @@ public sealed class Handoffs :
IReadOnlyDictionary<AIAgent, IEnumerable<Handoffs.HandoffTarget>>
{
/// <summary>
/// Initializes a new instance of the <see cref="Orchestration.Handoffs"/> class with no handoff relationships.
/// Initializes a new instance of the <see cref="Handoffs"/> class with no handoff relationships.
/// </summary>
/// <param name="initialAgent">The first agent to be invoked (prior to any handoff).</param>
private Handoffs(AIAgent initialAgent)
@@ -41,7 +41,7 @@ public sealed class Handoffs :
/// Creates a new collection of handoffs that start with the specified agent.
/// </summary>
/// <param name="initialAgent">The initial agent.</param>
/// <returns>The new <see cref="Orchestration.Handoffs"/> instance.</returns>
/// <returns>The new <see cref="Handoffs"/> instance.</returns>
public static Handoffs StartWith(AIAgent initialAgent) => new(initialAgent);
/// <summary>Creates a new <see cref="HandoffOrchestration"/> from the described handoffs.</summary>
@@ -54,7 +54,7 @@ public sealed class Handoffs :
/// </summary>
/// <param name="source">The source agent.</param>
/// <param name="targets">The target agents to add as handoff targets for the source agent.</param>
/// <returns>The updated <see cref="Orchestration.Handoffs"/> instance.</returns>
/// <returns>The updated <see cref="Handoffs"/> instance.</returns>
/// <remarks>The handoff reason for each target is derived from its description or name.</remarks>
public Handoffs Add(AIAgent source, AIAgent[] targets)
{
@@ -79,7 +79,7 @@ public sealed class Handoffs :
/// <param name="source">The source agent.</param>
/// <param name="target">The target agent.</param>
/// <param name="handoffReason">The reason the <paramref name="source"/> should hand off to the <paramref name="target"/>.</param>
/// <returns>The updated <see cref="Orchestration.Handoffs"/> instance.</returns>
/// <returns>The updated <see cref="Handoffs"/> instance.</returns>
public Handoffs Add(AIAgent source, AIAgent target, string? handoffReason = null)
{
Throw.IfNull(source);
@@ -127,7 +127,7 @@ public sealed class Handoffs :
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() =>
((IReadOnlyDictionary<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)this).GetEnumerator();
((IReadOnlyDictionary<AIAgent, IEnumerable<HandoffTarget>>)this).GetEnumerator();
/// <inheritdoc />
bool IReadOnlyDictionary<AIAgent, IEnumerable<HandoffTarget>>.TryGetValue(AIAgent key, out IEnumerable<HandoffTarget> value)
@@ -129,7 +129,7 @@ public abstract partial class OrchestratingAgent : AIAgent
CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cancellationToken = cts.Token;
JsonElement? checkpoint = await this.ReadCheckpointAsync(context, cancellationToken).ConfigureAwait(false);
JsonElement? checkpoint = await ReadCheckpointAsync(context, cancellationToken).ConfigureAwait(false);
Task<AgentRunResponse> completion = checkpoint is null ?
this.RunCoreAsync(readonlyCollectionMessages, context, cancellationToken) :
this.ResumeCoreAsync(checkpoint.Value, readonlyCollectionMessages, context, cancellationToken);
@@ -207,7 +207,7 @@ public abstract partial class OrchestratingAgent : AIAgent
/// <param name="context">The context for the orchestrating operation.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A Task that completes when the asynchronous operation quiesces.</returns>
protected async Task WriteCheckpointAsync(JsonElement state, OrchestratingAgentContext context, CancellationToken cancellationToken)
protected static async Task WriteCheckpointAsync(JsonElement state, OrchestratingAgentContext context, CancellationToken cancellationToken)
{
_ = Throw.IfNull(context);
@@ -236,7 +236,7 @@ public abstract partial class OrchestratingAgent : AIAgent
/// <param name="context">The context for the orchestrating operation.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The loaded state, or null if it doesn't exist.</returns>
protected async ValueTask<JsonElement?> ReadCheckpointAsync(OrchestratingAgentContext context, CancellationToken cancellationToken)
protected static async ValueTask<JsonElement?> ReadCheckpointAsync(OrchestratingAgentContext context, CancellationToken cancellationToken)
{
_ = Throw.IfNull(context);
@@ -276,10 +276,10 @@ public abstract partial class OrchestratingAgent : AIAgent
[LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} completed agent '{Agent}' ('{Id}')")]
private static partial void LogOrchestrationSubagentCompleted(ILogger logger, string orchestration, string id, string agent);
private protected void LogOrchestrationSubagentRunning(OrchestratingAgentContext context, AIAgent agent) =>
private protected static void LogOrchestrationSubagentRunning(OrchestratingAgentContext context, AIAgent agent) =>
LogOrchestrationSubagentRunning(context.Logger, context.ToString(), context.Id, agent.DisplayName);
private protected void LogOrchestrationSubagentCompleted(OrchestratingAgentContext context, AIAgent agent) =>
private protected static void LogOrchestrationSubagentCompleted(OrchestratingAgentContext context, AIAgent agent) =>
LogOrchestrationSubagentCompleted(context.Logger, context.ToString(), context.Id, agent.DisplayName);
private static async Task LogCompletionAsync(ILogger logger, OrchestratingAgentContext context, Task<AgentRunResponse> completion)
@@ -48,20 +48,20 @@ public sealed partial class SequentialOrchestration : OrchestratingAgent
AgentRunResponse? response = null;
for (; i < this.Agents.Count; i++)
{
this.LogOrchestrationSubagentRunning(context, this.Agents[i]);
LogOrchestrationSubagentRunning(context, this.Agents[i]);
response = await RunAsync(this.Agents[i], context, input, options: null, cancellationToken).ConfigureAwait(false);
input = response.Messages as IReadOnlyCollection<ChatMessage> ?? [.. response.Messages];
await this.CheckpointAsync(i + 1, input, context, cancellationToken).ConfigureAwait(false);
await CheckpointAsync(i + 1, input, context, cancellationToken).ConfigureAwait(false);
}
Debug.Assert(response is not null, "Response should not be null after processing a positive number of agents.");
return response!;
}
private Task CheckpointAsync(int index, IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
context.Runtime is not null ? base.WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(index, messages), OrchestrationJsonContext.Default.SequentialState), context, cancellationToken) :
private static Task CheckpointAsync(int index, IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
context.Runtime is not null ? WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(index, messages), OrchestrationJsonContext.Default.SequentialState), context, cancellationToken) :
Task.CompletedTask;
internal sealed record SequentialState(int Index, IReadOnlyCollection<ChatMessage> Messages);
@@ -90,12 +90,8 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
}
/// <inheritdoc/>
public override async Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default)
{
AIAgent agent = await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken).ConfigureAwait(false);
return agent;
}
public override async Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default) =>
await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken).ConfigureAwait(false);
/// <inheritdoc/>
public override async Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default)
@@ -31,7 +31,7 @@ public static class DeclarativeWorkflowBuilder
where TInput : notnull
{
using StreamReader yamlReader = File.OpenText(workflowFile);
return Build<TInput>(yamlReader, options, inputTransform);
return Build(yamlReader, options, inputTransform);
}
/// <summary>
@@ -8,14 +8,12 @@ internal static class BotElementExtensions
{
public static string? GetParentId(this BotElement element) => element.Parent?.GetId();
public static string GetId(this BotElement element)
{
return element switch
public static string GetId(this BotElement element) =>
element switch
{
DialogAction action => action.Id.Value,
ConditionItem conditionItem => conditionItem.Id ?? throw new DeclarativeModelException($"Undefined identifier for {nameof(ConditionItem)} that is member of {conditionItem.GetParentId() ?? "(root)"}."),
OnActivity activity => activity.Id.Value,
_ => throw new DeclarativeModelException($"Unknown identify for element type: {element.GetType().Name}"),
};
}
}
@@ -13,7 +13,7 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions;
internal static class ChatMessageExtensions
{
public static RecordValue ToRecord(this ChatMessage message) =>
RecordValue.NewRecordFromFields(message.GetMessageFields());
FormulaValue.NewRecordFromFields(message.GetMessageFields());
public static TableValue ToTable(this IEnumerable<ChatMessage> messages) =>
FormulaValue.NewTable(s_messageRecordType, messages.Select(message => message.ToRecord()));
@@ -14,7 +14,7 @@ internal static class DataValueExtensions
value switch
{
null => FormulaValue.NewBlank(),
BlankDataValue => BlankValue.NewBlank(),
BlankDataValue => FormulaValue.NewBlank(),
BooleanDataValue boolValue => FormulaValue.New(boolValue.Value),
NumberDataValue numberValue => FormulaValue.New(numberValue.Value),
FloatDataValue floatValue => FormulaValue.New(floatValue.Value),
@@ -71,8 +71,8 @@ internal static class FormulaValueExtensions
DateTimeValue datetimeValue => DateTimeDataValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
TimeValue timeValue => TimeDataValue.Create(timeValue.Value),
StringValue stringValue => StringDataValue.Create(stringValue.Value),
BlankValue blankValue => DataValue.Blank(),
VoidValue voidValue => DataValue.Blank(),
BlankValue => DataValue.Blank(),
VoidValue => DataValue.Blank(),
RecordValue recordValue => recordValue.ToRecord(),
TableValue tableValue => tableValue.ToTable(),
_ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"),
@@ -141,10 +141,10 @@ internal static class FormulaValueExtensions
};
public static TableDataValue ToTable(this TableValue value) =>
TableDataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToRecord()).ToImmutableArray());
DataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToRecord()).ToImmutableArray());
public static RecordDataValue ToRecord(this RecordValue value) =>
RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()));
DataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()));
private static RecordValue ToRecord(this IDictionary value)
{
@@ -229,7 +229,7 @@ internal static class FormulaValueExtensions
GuidValue guidValue => JsonValue.Create(guidValue.Value),
RecordValue recordValue => recordValue.ToJson(),
TableValue tableValue => tableValue.ToJson(),
BlankValue blankValue => JsonValue.Create(string.Empty),
BlankValue => JsonValue.Create(string.Empty),
_ => $"[{value.GetType().Name}]",
};
@@ -23,12 +23,12 @@ internal static class RecordDataTypeExtensions
FormulaValue? parsedValue =
property.Value.Type switch
{
StringDataType => StringValue.New(propertyElement.GetString()),
NumberDataType => NumberValue.New(propertyElement.GetDecimal()),
BooleanDataType => BooleanValue.New(propertyElement.GetBoolean()),
DateTimeDataType => DateTimeValue.New(propertyElement.GetDateTime()),
DateDataType => DateValue.New(propertyElement.GetDateTime()),
TimeDataType => TimeValue.New(propertyElement.GetDateTimeOffset().TimeOfDay),
StringDataType => FormulaValue.New(propertyElement.GetString()),
NumberDataType => FormulaValue.New(propertyElement.GetDecimal()),
BooleanDataType => FormulaValue.New(propertyElement.GetBoolean()),
DateTimeDataType => FormulaValue.New(propertyElement.GetDateTime()),
DateDataType => FormulaValue.New(propertyElement.GetDateTime()),
TimeDataType => FormulaValue.New(propertyElement.GetDateTimeOffset().TimeOfDay),
RecordDataType recordType => recordType.ParseRecord(propertyElement),
TableDataType tableType => ParseTable(tableType, propertyElement),
_ => throw new InvalidOperationException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'"),
@@ -5,19 +5,24 @@ using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
internal static class StringExtensions
internal static partial class StringExtensions
{
private static readonly Regex s_regex = new(@"^```(?:\w*)\s*([\s\S]*?)\s*```$", RegexOptions.Compiled | RegexOptions.Multiline);
#if NET
[GeneratedRegex(@"^```(?:\w*)\s*([\s\S]*?)\s*```$", RegexOptions.Multiline)]
private static partial Regex TrimJsonDelimiterRegex();
#else
private static Regex TrimJsonDelimiterRegex() => s_trimJsonDelimiterRegex;
private static readonly Regex s_trimJsonDelimiterRegex = new(@"^```(?:\w*)\s*([\s\S]*?)\s*```$", RegexOptions.Compiled | RegexOptions.Multiline);
#endif
public static string TrimJsonDelimiter(this string value)
{
Match match = s_regex.Match(value.Trim());
if (match.Success)
{
return match.Groups[1].Value.Trim();
}
value = value.Trim();
return value.Trim();
Match match = TrimJsonDelimiterRegex().Match(value);
return match.Success ?
match.Groups[1].Value.Trim() :
value;
}
public static FormulaValue ToFormula(this string? value) =>
@@ -4,21 +4,18 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
internal static class TemplateExtensions
{
public static string Format(this RecalcEngine engine, IEnumerable<TemplateLine> template)
{
return string.Concat(template.Select(line => engine.Format(line)));
}
public static string Format(this RecalcEngine engine, IEnumerable<TemplateLine> template) =>
string.Concat(template.Select(engine.Format));
public static string Format(this RecalcEngine engine, TemplateLine? line)
{
return string.Concat(line?.Segments.Select(segment => engine.Format(segment)) ?? [string.Empty]);
}
public static string Format(this RecalcEngine engine, TemplateLine? line) =>
line is not null ?
string.Concat(line.Segments.Select(engine.Format)) :
string.Empty;
public static string Format(this RecalcEngine engine, TemplateSegment segment)
{
@@ -27,20 +24,16 @@ internal static class TemplateExtensions
return textSegment.Value ?? string.Empty;
}
if (segment is ExpressionSegment expressionSegment)
if (segment is ExpressionSegment { Expression: not null } expressionSegment)
{
if (expressionSegment.Expression is not null)
if (expressionSegment.Expression.ExpressionText is not null)
{
if (expressionSegment.Expression.ExpressionText is not null)
{
FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.ExpressionText);
return expressionValue.Format();
}
if (expressionSegment.Expression.VariableReference is not null)
{
FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.VariableReference.ToString());
return expressionValue.Format();
}
return engine.Eval(expressionSegment.Expression.ExpressionText).Format();
}
if (expressionSegment.Expression.VariableReference is not null)
{
return engine.Eval(expressionSegment.Expression.VariableReference.ToString()).Format();
}
}
@@ -23,7 +23,7 @@ internal sealed class DeclarativeWorkflowModel
public int GetDepth(string? nodeId)
{
if (nodeId == null)
if (nodeId is null)
{
return 0;
}
@@ -112,15 +112,7 @@ internal sealed class DeclarativeWorkflowModel
workflowBuilder.AddEdge(GetExecutorIsh(link.Source), GetExecutorIsh(targetNode), link.Condition);
}
ExecutorIsh GetExecutorIsh(ModelNode node)
{
if (node.Port is not null)
{
return node.Port;
}
return node.Executor;
}
static ExecutorIsh GetExecutorIsh(ModelNode node) => node.Port ?? (ExecutorIsh)node.Executor;
}
private ModelNode DefineNode(Executor executor, ModelNode? parentNode = null, Action? completionHandler = null)
@@ -148,7 +140,7 @@ internal sealed class DeclarativeWorkflowModel
return null;
}
while (itemId != null)
while (itemId is not null)
{
if (!this.Nodes.TryGetValue(itemId, out ModelNode? itemNode))
{
@@ -180,7 +172,7 @@ internal sealed class DeclarativeWorkflowModel
public List<ModelNode> Children { get; } = [];
public int Depth => this.Parent?.Depth + 1 ?? 0;
public int Depth => (this.Parent?.Depth + 1) ?? 0;
public Action? CompletionHandler => completionHandler;
}
@@ -137,7 +137,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
// Create conditional link for else action
string stepId = ConditionGroupExecutor.Steps.Else(item);
this._workflowModel.AddLink(action.Id, stepId, (result) => action.IsElse(result));
this._workflowModel.AddLink(action.Id, stepId, action.IsElse);
}
}
@@ -355,170 +355,74 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
this.NotSupported(item);
}
protected override void Visit(GetActivityMembers item)
{
this.NotSupported(item);
}
protected override void Visit(GetActivityMembers item) => this.NotSupported(item);
protected override void Visit(UpdateActivity item)
{
this.NotSupported(item);
}
protected override void Visit(UpdateActivity item) => this.NotSupported(item);
protected override void Visit(ActivateExternalTrigger item)
{
this.NotSupported(item);
}
protected override void Visit(ActivateExternalTrigger item) => this.NotSupported(item);
protected override void Visit(DisableTrigger item)
{
this.NotSupported(item);
}
protected override void Visit(DisableTrigger item) => this.NotSupported(item);
protected override void Visit(WaitForConnectorTrigger item)
{
this.NotSupported(item);
}
protected override void Visit(WaitForConnectorTrigger item) => this.NotSupported(item);
protected override void Visit(InvokeConnectorAction item)
{
this.NotSupported(item);
}
protected override void Visit(InvokeConnectorAction item) => this.NotSupported(item);
protected override void Visit(InvokeCustomModelAction item)
{
this.NotSupported(item);
}
protected override void Visit(InvokeCustomModelAction item) => this.NotSupported(item);
protected override void Visit(InvokeFlowAction item)
{
this.NotSupported(item);
}
protected override void Visit(InvokeFlowAction item) => this.NotSupported(item);
protected override void Visit(InvokeAIBuilderModelAction item)
{
this.NotSupported(item);
}
protected override void Visit(InvokeAIBuilderModelAction item) => this.NotSupported(item);
protected override void Visit(InvokeSkillAction item)
{
this.NotSupported(item);
}
protected override void Visit(InvokeSkillAction item) => this.NotSupported(item);
protected override void Visit(AdaptiveCardPrompt item)
{
this.NotSupported(item);
}
protected override void Visit(AdaptiveCardPrompt item) => this.NotSupported(item);
protected override void Visit(CSATQuestion item)
{
this.NotSupported(item);
}
protected override void Visit(OAuthInput item)
{
this.NotSupported(item);
}
protected override void Visit(OAuthInput item) => this.NotSupported(item);
protected override void Visit(BeginDialog item)
{
this.NotSupported(item);
}
protected override void Visit(BeginDialog item) => this.NotSupported(item);
protected override void Visit(UnknownDialogAction item)
{
this.NotSupported(item);
}
protected override void Visit(UnknownDialogAction item) => this.NotSupported(item);
protected override void Visit(EndDialog item)
{
this.NotSupported(item);
}
protected override void Visit(EndDialog item) => this.NotSupported(item);
protected override void Visit(RepeatDialog item)
{
this.NotSupported(item);
}
protected override void Visit(RepeatDialog item) => this.NotSupported(item);
protected override void Visit(ReplaceDialog item)
{
this.NotSupported(item);
}
protected override void Visit(ReplaceDialog item) => this.NotSupported(item);
protected override void Visit(CancelAllDialogs item)
{
this.NotSupported(item);
}
protected override void Visit(CancelAllDialogs item) => this.NotSupported(item);
protected override void Visit(CancelDialog item)
{
this.NotSupported(item);
}
protected override void Visit(CancelDialog item) => this.NotSupported(item);
protected override void Visit(EmitEvent item)
{
this.NotSupported(item);
}
protected override void Visit(EmitEvent item) => this.NotSupported(item);
protected override void Visit(GetConversationMembers item)
{
this.NotSupported(item);
}
protected override void Visit(GetConversationMembers item) => this.NotSupported(item);
protected override void Visit(HttpRequestAction item)
{
this.NotSupported(item);
}
protected override void Visit(HttpRequestAction item) => this.NotSupported(item);
protected override void Visit(RecognizeIntent item)
{
this.NotSupported(item);
}
protected override void Visit(RecognizeIntent item) => this.NotSupported(item);
protected override void Visit(TransferConversation item)
{
this.NotSupported(item);
}
protected override void Visit(TransferConversation item) => this.NotSupported(item);
protected override void Visit(TransferConversationV2 item)
{
this.NotSupported(item);
}
protected override void Visit(TransferConversationV2 item) => this.NotSupported(item);
protected override void Visit(SignOutUser item)
{
this.NotSupported(item);
}
protected override void Visit(SignOutUser item) => this.NotSupported(item);
protected override void Visit(LogCustomTelemetryEvent item)
{
this.NotSupported(item);
}
protected override void Visit(LogCustomTelemetryEvent item) => this.NotSupported(item);
protected override void Visit(DisconnectedNodeContainer item)
{
this.NotSupported(item);
}
protected override void Visit(DisconnectedNodeContainer item) => this.NotSupported(item);
protected override void Visit(CreateSearchQuery item)
{
this.NotSupported(item);
}
protected override void Visit(CreateSearchQuery item) => this.NotSupported(item);
protected override void Visit(SearchKnowledgeSources item)
{
this.NotSupported(item);
}
protected override void Visit(SearchKnowledgeSources item) => this.NotSupported(item);
protected override void Visit(SearchAndSummarizeWithCustomModel item)
{
this.NotSupported(item);
}
protected override void Visit(SearchAndSummarizeWithCustomModel item) => this.NotSupported(item);
protected override void Visit(SearchAndSummarizeContent item)
{
this.NotSupported(item);
}
protected override void Visit(SearchAndSummarizeContent item) => this.NotSupported(item);
#endregion
@@ -563,10 +467,8 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
this.HasUnsupportedActions = true;
}
private void Trace(BotElement item)
{
private void Trace(BotElement item) =>
Debug.WriteLine($"> VISIT: {new string('\t', this._workflowModel.GetDepth(item.GetParentId()))}{FormatItem(item)} => {FormatParent(item)}");
}
private void Trace(DialogAction item)
{
@@ -575,6 +477,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
{
parentId = Steps.Root(parentId);
}
Debug.WriteLine($"> VISIT: {new string('\t', this._workflowModel.GetDepth(parentId))}{FormatItem(item)} => {FormatParent(item)}");
}
@@ -15,7 +15,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Workflo
{
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
EvaluationResult<VariablesToClearWrapper> variablesResult = this.State.Evaluator.GetValue<VariablesToClearWrapper>(this.Model.Variables);
EvaluationResult<VariablesToClearWrapper> variablesResult = this.State.Evaluator.GetValue(this.Model.Variables);
variablesResult.Value.Handle(new ScopeHandler(this.Id, this.State));
@@ -24,20 +24,16 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Workflo
private sealed class ScopeHandler(string executorId, WorkflowFormulaState state) : IEnumVariablesToClearHandler
{
public void HandleAllGlobalVariables()
{
public void HandleAllGlobalVariables() =>
this.ClearAll(VariableScopeNames.Global);
}
public void HandleConversationHistory()
{
// Not supported....
}
public void HandleConversationScopedVariables()
{
public void HandleConversationScopedVariables() =>
this.ClearAll(WorkflowFormulaState.DefaultScopeName);
}
public void HandleUnknownValue()
{
@@ -47,9 +47,7 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
return string.Equals(Steps.Else(this.Model), executorMessage.Result as string, StringComparison.Ordinal);
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
for (int index = 0; index < this.Model.Conditions.Length; ++index)
{
@@ -69,8 +67,6 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
return Steps.Else(this.Model);
}
public async ValueTask DoneAsync(IWorkflowContext context, ExecutorResultMessage _, CancellationToken cancellationToken)
{
public async ValueTask DoneAsync(IWorkflowContext context, ExecutorResultMessage _, CancellationToken cancellationToken) =>
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
}
}
@@ -61,7 +61,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false))
{
await AssignConversationId(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false);
await AssignConversationIdAsync(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false);
if (autoSend)
{
@@ -72,7 +72,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
}
}
async ValueTask AssignConversationId(string? assignValue)
async ValueTask AssignConversationIdAsync(string? assignValue)
{
if (assignValue is not null && conversationId is null)
{
@@ -42,9 +42,9 @@ internal sealed class ParseValueExecutor(ParseValue model, WorkflowFormulaState
parsedResult =
this.Model.ValueType switch
{
StringDataType => StringValue.New(stringValue.Value),
NumberDataType => NumberValue.New(stringValue.Value),
BooleanDataType => BooleanValue.New(stringValue.Value),
StringDataType => FormulaValue.New(stringValue.Value),
NumberDataType => FormulaValue.New(stringValue.Value),
BooleanDataType => FormulaValue.New(stringValue.Value),
RecordDataType recordType => ParseRecord(recordType, stringValue.Value),
_ => null
};
@@ -63,11 +63,10 @@ internal sealed class ParseValueExecutor(ParseValue model, WorkflowFormulaState
RecordValue ParseRecord(RecordDataType recordType, string rawText)
{
string jsonText = rawText.TrimJsonDelimiter();
JsonDocument json = JsonDocument.Parse(jsonText);
JsonElement currentElement = json.RootElement;
using JsonDocument json = JsonDocument.Parse(jsonText);
try
{
return recordType.ParseRecord(currentElement);
return recordType.ParseRecord(json.RootElement);
}
catch (Exception exception)
{
@@ -15,7 +15,7 @@ internal sealed class ResetVariableExecutor(ResetVariable model, WorkflowFormula
{
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
PropertyPath variablePath = Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}");
Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}");
this.State.Reset(this.Model.Variable);
Debug.WriteLine(
@@ -2,7 +2,6 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Agents.Workflows.Declarative.Extensions;
using Microsoft.Bot.ObjectModel;
@@ -31,22 +30,20 @@ internal static class SystemScope
public const string UserLanguage = nameof(UserLanguage);
}
public static FrozenSet<string> AllNames { get; } = GetNames().ToFrozenSet();
public static IEnumerable<string> GetNames()
{
yield return Names.Activity;
yield return Names.Bot;
yield return Names.Conversation;
yield return Names.ConversationId;
yield return Names.InternalId;
yield return Names.LastMessage;
yield return Names.LastMessageId;
yield return Names.LastMessageText;
yield return Names.Recognizer;
yield return Names.User;
yield return Names.UserLanguage;
}
public static FrozenSet<string> AllNames { get; } =
[
Names.Activity,
Names.Bot,
Names.Conversation,
Names.ConversationId,
Names.InternalId,
Names.LastMessage,
Names.LastMessageId,
Names.LastMessageText,
Names.Recognizer,
Names.User,
Names.UserLanguage,
];
public static void InitializeSystem(this WorkflowFormulaState scopes)
{
@@ -59,7 +56,7 @@ internal static class SystemScope
scopes.Set(
Names.Conversation,
RecordValue.NewRecordFromFields(
FormulaValue.NewRecordFromFields(
new NamedValue("Id", FormulaType.String.NewBlank()),
new NamedValue("LocalTimeZone", FormulaValue.New(TimeZoneInfo.Local.StandardName)),
new NamedValue("LocalTimeZoneOffset", FormulaValue.New(TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow))),
@@ -70,17 +67,17 @@ internal static class SystemScope
scopes.Set(
Names.Recognizer,
RecordValue.NewRecordFromFields(
FormulaValue.NewRecordFromFields(
new NamedValue("Id", FormulaType.String.NewBlank()),
new NamedValue("Text", FormulaType.String.NewBlank())),
VariableScopeNames.System);
scopes.Set(
Names.User,
RecordValue.NewRecordFromFields(
new NamedValue("Language", StringValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName))),
FormulaValue.NewRecordFromFields(
new NamedValue("Language", FormulaValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName))),
VariableScopeNames.System);
scopes.Set(Names.UserLanguage, StringValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName), VariableScopeNames.System);
scopes.Set(Names.UserLanguage, FormulaValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName), VariableScopeNames.System);
void Set(string key, string? value = null)
{

Some files were not shown because too many files have changed in this diff Show More