Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
</Folder>
<Folder Name="/Samples/AGUIClientServer/">
<Project Path="samples/AGUIClientServer/AGUIClient/AGUIClient.csproj" />
<Project Path="samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
<Project Path="samples/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>b9c3f1e1-2fb4-5g29-0e52-53e2b7g9gf21</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace AGUIDojoServer;

[JsonSerializable(typeof(WeatherInfo))]
[JsonSerializable(typeof(Recipe))]
[JsonSerializable(typeof(Ingredient))]
[JsonSerializable(typeof(RecipeResponse))]
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.

using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ChatClient = OpenAI.Chat.ChatClient;

namespace AGUIDojoServer;

internal static class ChatClientAgentFactory
{
private static AzureOpenAIClient? s_azureOpenAIClient;
private static string? s_deploymentName;

public static void Initialize(IConfiguration configuration)
{
string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");

s_azureOpenAIClient = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential());
}

public static ChatClientAgent CreateAgenticChat()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);

return chatClient.AsIChatClient().CreateAIAgent(
name: "AgenticChat",
description: "A simple chat agent using Azure OpenAI");
}

public static ChatClientAgent CreateBackendToolRendering()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);

return chatClient.AsIChatClient().CreateAIAgent(
name: "BackendToolRenderer",
description: "An agent that can render backend tools using Azure OpenAI",
tools: [AIFunctionFactory.Create(
GetWeather,
name: "get_weather",
description: "Get the weather for a given location.",
AGUIDojoServerSerializerContext.Default.Options)]);
}

public static ChatClientAgent CreateHumanInTheLoop()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);

return chatClient.AsIChatClient().CreateAIAgent(
name: "HumanInTheLoopAgent",
description: "An agent that involves human feedback in its decision-making process using Azure OpenAI");
}

public static ChatClientAgent CreateToolBasedGenerativeUI()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);

return chatClient.AsIChatClient().CreateAIAgent(
name: "ToolBasedGenerativeUIAgent",
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
}

public static ChatClientAgent CreateAgenticUI()
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);

return chatClient.AsIChatClient().CreateAIAgent(
name: "AgenticUIAgent",
description: "An agent that generates agentic user interfaces using Azure OpenAI");
}

public static AIAgent CreateSharedState(JsonSerializerOptions options)
{
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);

var baseAgent = chatClient.AsIChatClient().CreateAIAgent(
name: "SharedStateAgent",
description: "An agent that demonstrates shared state patterns using Azure OpenAI");

return new SharedStateAgent(baseAgent, options);
}

[Description("Get the weather for a given location.")]
private static WeatherInfo GetWeather([Description("The location to get the weather for.")] string location) => new()
{
Temperature = 20,
Conditions = "sunny",
Humidity = 50,
WindSpeed = 10,
FeelsLike = 25
};
}
17 changes: 17 additions & 0 deletions dotnet/samples/AGUIClientServer/AGUIDojoServer/Ingredient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace AGUIDojoServer;

internal sealed class Ingredient
{
[JsonPropertyName("icon")]
public string Icon { get; set; } = string.Empty;

[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;

[JsonPropertyName("amount")]
public string Amount { get; set; } = string.Empty;
}
45 changes: 45 additions & 0 deletions dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.

using AGUIDojoServer;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.Extensions.Options;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody
| HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody;
logging.RequestBodyLogLimit = int.MaxValue;
logging.ResponseBodyLogLimit = int.MaxValue;
});

builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIDojoServerSerializerContext.Default));
builder.Services.AddAGUI();

WebApplication app = builder.Build();

app.UseHttpLogging();

// Initialize the factory
ChatClientAgentFactory.Initialize(app.Configuration);

// Map the AG-UI agent endpoints for different scenarios
app.MapAGUI("/agentic_chat", ChatClientAgentFactory.CreateAgenticChat());

app.MapAGUI("/backend_tool_rendering", ChatClientAgentFactory.CreateBackendToolRendering());

app.MapAGUI("/human_in_the_loop", ChatClientAgentFactory.CreateHumanInTheLoop());

app.MapAGUI("/tool_based_generative_ui", ChatClientAgentFactory.CreateToolBasedGenerativeUI());

app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI());

var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
app.MapAGUI("/shared_state", ChatClientAgentFactory.CreateSharedState(jsonOptions.Value.SerializerOptions));

await app.RunAsync();

public partial class Program { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"AGUIDojoServer": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5018"
}
}
}
26 changes: 26 additions & 0 deletions dotnet/samples/AGUIClientServer/AGUIDojoServer/Recipe.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace AGUIDojoServer;

internal sealed class Recipe
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;

[JsonPropertyName("skill_level")]
public string SkillLevel { get; set; } = string.Empty;

[JsonPropertyName("cooking_time")]
public string CookingTime { get; set; } = string.Empty;

[JsonPropertyName("special_preferences")]
public List<string> SpecialPreferences { get; set; } = [];

[JsonPropertyName("ingredients")]
public List<Ingredient> Ingredients { get; set; } = [];

[JsonPropertyName("instructions")]
public List<string> Instructions { get; set; } = [];
}
13 changes: 13 additions & 0 deletions dotnet/samples/AGUIClientServer/AGUIDojoServer/RecipeResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace AGUIDojoServer;

#pragma warning disable CA1812 // Used for the JsonSchema response format
internal sealed class RecipeResponse
#pragma warning restore CA1812
{
[JsonPropertyName("recipe")]
public Recipe Recipe { get; set; } = new();
}
106 changes: 106 additions & 0 deletions dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedStateAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

namespace AGUIDojoServer;

[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateSharedState")]
internal sealed class SharedStateAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;

public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}

public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}

public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
!properties.TryGetValue("ag_ui_state", out JsonElement state))
{
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
yield break;
}

var firstRunOptions = new ChatClientAgentRunOptions
{
ChatOptions = chatRunOptions.ChatOptions.Clone(),
AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
ContinuationToken = chatRunOptions.ContinuationToken,
ChatClientFactory = chatRunOptions.ChatClientFactory,
};

// Configure JSON schema response format for structured state output
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<RecipeResponse>(
schemaName: "RecipeResponse",
schemaDescription: "A response containing a recipe with title, skill level, cooking time, preferences, ingredients, and instructions");

ChatMessage stateUpdateMessage = new(
ChatRole.System,
[
new TextContent("Here is the current state in JSON format:"),
new TextContent(state.GetRawText()),
new TextContent("The new state is:")
]);

var firstRunMessages = messages.Append(stateUpdateMessage);

var allUpdates = new List<AgentRunResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);

// Yield all non-text updates (tool calls, etc.)
bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
if (hasNonTextContent)
{
yield return update;
}
}

var response = allUpdates.ToAgentRunResponse();

if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
{
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateSnapshot,
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
yield return new AgentRunResponseUpdate
{
Contents = [new DataContent(stateBytes, "application/json")]
};
}
else
{
yield break;
}

var secondRunMessages = messages.Concat(response.Messages).Append(
new ChatMessage(
ChatRole.System,
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));

await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
}
23 changes: 23 additions & 0 deletions dotnet/samples/AGUIClientServer/AGUIDojoServer/WeatherInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace AGUIDojoServer;

internal sealed class WeatherInfo
{
[JsonPropertyName("temperature")]
public int Temperature { get; init; }

[JsonPropertyName("conditions")]
public string Conditions { get; init; } = string.Empty;

[JsonPropertyName("humidity")]
public int Humidity { get; init; }

[JsonPropertyName("wind_speed")]
public int WindSpeed { get; init; }

[JsonPropertyName("feelsLike")]
public int FeelsLike { get; init; }
}
Loading
Loading