test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
This commit is contained in:
21
dotnet/samples/AGUIClientServer/AGUIClient/AGUIClient.csproj
Normal file
21
dotnet/samples/AGUIClientServer/AGUIClient/AGUIClient.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
|
||||
// and display streaming updates including conversation/response metadata, text content, and errors.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIClient;
|
||||
|
||||
[JsonSerializable(typeof(SensorRequest))]
|
||||
[JsonSerializable(typeof(SensorResponse))]
|
||||
internal sealed partial class AGUIClientSerializerContext : JsonSerializerContext;
|
||||
213
dotnet/samples/AGUIClientServer/AGUIClient/Program.cs
Normal file
213
dotnet/samples/AGUIClientServer/AGUIClient/Program.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
|
||||
// and display streaming updates including conversation/response metadata, text content, and errors.
|
||||
|
||||
using System.CommandLine;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AGUI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AGUIClient;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Create root command with options
|
||||
RootCommand rootCommand = new("AGUIClient");
|
||||
rootCommand.SetAction((_, ct) => HandleCommandsAsync(ct));
|
||||
|
||||
// Run the command
|
||||
return await rootCommand.Parse(args).InvokeAsync();
|
||||
}
|
||||
|
||||
private static async Task HandleCommandsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Set up the logging
|
||||
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Information);
|
||||
});
|
||||
ILogger logger = loggerFactory.CreateLogger("AGUIClient");
|
||||
|
||||
// Retrieve configuration settings
|
||||
IConfigurationRoot configRoot = new ConfigurationBuilder()
|
||||
.AddEnvironmentVariables()
|
||||
.AddUserSecrets(Assembly.GetExecutingAssembly())
|
||||
.Build();
|
||||
|
||||
string serverUrl = configRoot["AGUI_SERVER_URL"] ?? "http://localhost:5100";
|
||||
|
||||
logger.LogInformation("Connecting to AG-UI server at: {ServerUrl}", serverUrl);
|
||||
|
||||
// Create the AG-UI client agent
|
||||
using HttpClient httpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
var changeBackground = AIFunctionFactory.Create(
|
||||
() =>
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkBlue;
|
||||
Console.WriteLine("Changing color to blue");
|
||||
},
|
||||
name: "change_background_color",
|
||||
description: "Change the console background color to dark blue."
|
||||
);
|
||||
|
||||
var readClientClimateSensors = AIFunctionFactory.Create(
|
||||
([Description("The sensors measurements to include in the response")] SensorRequest request) =>
|
||||
{
|
||||
return new SensorResponse()
|
||||
{
|
||||
Temperature = 22.5,
|
||||
Humidity = 45.0,
|
||||
AirQualityIndex = 75
|
||||
};
|
||||
},
|
||||
name: "read_client_climate_sensors",
|
||||
description: "Reads the climate sensor data from the client device.",
|
||||
serializerOptions: AGUIClientSerializerContext.Default.Options
|
||||
);
|
||||
|
||||
var chatClient = new AGUIChatClient(
|
||||
httpClient,
|
||||
serverUrl,
|
||||
jsonSerializerOptions: AGUIClientSerializerContext.Default.Options);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent",
|
||||
tools: [changeBackground, readClientClimateSensors]);
|
||||
|
||||
AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
|
||||
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get user message
|
||||
Console.Write("\nUser (:q or quit to exit): ");
|
||||
string? message = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
Console.WriteLine("Request cannot be empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message is ":q" or "quit")
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
messages.Add(new(ChatRole.User, message));
|
||||
|
||||
// Call RunStreamingAsync to get streaming updates
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
|
||||
{
|
||||
// Use AsChatResponseUpdate to access ChatResponseUpdate properties
|
||||
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
|
||||
updates.Add(chatUpdate);
|
||||
if (chatUpdate.ConversationId != null)
|
||||
{
|
||||
threadId = chatUpdate.ConversationId;
|
||||
}
|
||||
|
||||
// Display run started information from the first update
|
||||
if (isFirstUpdate && threadId != null && update.ResponseId != null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {update.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
|
||||
// Display different content types with appropriate formatting
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write(textContent.Text);
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case FunctionCallContent functionCallContent:
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Function Call - Name: {functionCallContent.Name}, Arguments: {PrintArguments(functionCallContent.Arguments)}]");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case FunctionResultContent functionResultContent:
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
if (functionResultContent.Exception != null)
|
||||
{
|
||||
Console.WriteLine($"\n[Function Result - Exception: {functionResultContent.Exception}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"\n[Function Result - Result: {functionResultContent.Result}]");
|
||||
}
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case ErrorContent errorContent:
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
string code = errorContent.AdditionalProperties?["Code"] as string ?? "Unknown";
|
||||
Console.WriteLine($"\n[Error - Code: {code}, Message: {errorContent.Message}]");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updates.Count > 0 && !updates[^1].Contents.Any(c => c is TextContent))
|
||||
{
|
||||
var lastUpdate = updates[^1];
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"[Run Ended - Thread: {threadId}, Run: {lastUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
messages.Clear();
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogInformation("AGUIClient operation was canceled.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not OutOfMemoryException and not StackOverflowException and not ThreadAbortException and not AccessViolationException)
|
||||
{
|
||||
logger.LogError(ex, "An error occurred while running the AGUIClient");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static string PrintArguments(IDictionary<string, object?>? arguments)
|
||||
{
|
||||
if (arguments == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
var builder = new StringBuilder().AppendLine();
|
||||
foreach (var kvp in arguments)
|
||||
{
|
||||
builder
|
||||
.AppendLine($" Name: {kvp.Key}")
|
||||
.AppendLine($" Value: {kvp.Value}");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
34
dotnet/samples/AGUIClientServer/AGUIClient/README.md
Normal file
34
dotnet/samples/AGUIClientServer/AGUIClient/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# AG-UI Client
|
||||
|
||||
This is a console application that demonstrates how to connect to an AG-UI server and interact with remote agents using the AG-UI protocol.
|
||||
|
||||
## Features
|
||||
|
||||
- Connects to an AG-UI server endpoint
|
||||
- Displays streaming updates with color-coded output:
|
||||
- **Yellow**: Run started notifications
|
||||
- **Cyan**: Agent text responses (streamed)
|
||||
- **Green**: Run finished notifications
|
||||
- **Red**: Error messages (if any)
|
||||
- Interactive prompt loop for sending messages
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variable to specify the AG-UI server URL:
|
||||
|
||||
```powershell
|
||||
$env:AGUI_SERVER_URL="http://localhost:5100"
|
||||
```
|
||||
|
||||
If not set, the default is `http://localhost:5100`.
|
||||
|
||||
## Running the Client
|
||||
|
||||
1. Make sure the AG-UI server is running
|
||||
2. Run the client:
|
||||
```bash
|
||||
cd AGUIClient
|
||||
dotnet run
|
||||
```
|
||||
3. Enter your messages and observe the streaming updates
|
||||
4. Type `:q` or `quit` to exit
|
||||
13
dotnet/samples/AGUIClientServer/AGUIClient/SensorRequest.cs
Normal file
13
dotnet/samples/AGUIClientServer/AGUIClient/SensorRequest.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
|
||||
// and display streaming updates including conversation/response metadata, text content, and errors.
|
||||
|
||||
namespace AGUIClient;
|
||||
|
||||
internal sealed class SensorRequest
|
||||
{
|
||||
public bool IncludeTemperature { get; set; } = true;
|
||||
public bool IncludeHumidity { get; set; } = true;
|
||||
public bool IncludeAirQualityIndex { get; set; } = true;
|
||||
}
|
||||
13
dotnet/samples/AGUIClientServer/AGUIClient/SensorResponse.cs
Normal file
13
dotnet/samples/AGUIClientServer/AGUIClient/SensorResponse.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
|
||||
// and display streaming updates including conversation/response metadata, text content, and errors.
|
||||
|
||||
namespace AGUIClient;
|
||||
|
||||
internal sealed class SensorResponse
|
||||
{
|
||||
public double Temperature { get; set; }
|
||||
public double Humidity { get; set; }
|
||||
public int AirQualityIndex { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>b9c3f1e1-2fb4-5g29-0e52-53e2b7g9gf21</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</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>
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using AGUIDojoServer.AgenticUI;
|
||||
using AGUIDojoServer.BackendToolRendering;
|
||||
using AGUIDojoServer.PredictiveStateUpdates;
|
||||
using AGUIDojoServer.SharedState;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
[JsonSerializable(typeof(WeatherInfo))]
|
||||
[JsonSerializable(typeof(Recipe))]
|
||||
[JsonSerializable(typeof(Ingredient))]
|
||||
[JsonSerializable(typeof(RecipeResponse))]
|
||||
[JsonSerializable(typeof(Plan))]
|
||||
[JsonSerializable(typeof(Step))]
|
||||
[JsonSerializable(typeof(StepStatus))]
|
||||
[JsonSerializable(typeof(StepStatus?))]
|
||||
[JsonSerializable(typeof(JsonPatchOperation))]
|
||||
[JsonSerializable(typeof(List<JsonPatchOperation>))]
|
||||
[JsonSerializable(typeof(List<string>))]
|
||||
[JsonSerializable(typeof(DocumentState))]
|
||||
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace AGUIDojoServer.AgenticUI;
|
||||
|
||||
internal static class AgenticPlanningTools
|
||||
{
|
||||
[Description("Create a plan with multiple steps.")]
|
||||
public static Plan CreatePlan([Description("List of step descriptions to create the plan.")] List<string> steps)
|
||||
{
|
||||
return new Plan
|
||||
{
|
||||
Steps = [.. steps.Select(s => new Step { Description = s, Status = StepStatus.Pending })]
|
||||
};
|
||||
}
|
||||
|
||||
[Description("Update a step in the plan with new description or status.")]
|
||||
public static async Task<List<JsonPatchOperation>> UpdatePlanStepAsync(
|
||||
[Description("The index of the step to update.")] int index,
|
||||
[Description("The new description for the step (optional).")] string? description = null,
|
||||
[Description("The new status for the step (optional).")] StepStatus? status = null)
|
||||
{
|
||||
var changes = new List<JsonPatchOperation>();
|
||||
|
||||
if (description is not null)
|
||||
{
|
||||
changes.Add(new JsonPatchOperation
|
||||
{
|
||||
Op = "replace",
|
||||
Path = $"/steps/{index}/description",
|
||||
Value = description
|
||||
});
|
||||
}
|
||||
|
||||
if (status.HasValue)
|
||||
{
|
||||
// Status must be lowercase to match AG-UI frontend expectations: "pending" or "completed"
|
||||
string statusValue = status.Value == StepStatus.Pending ? "pending" : "completed";
|
||||
changes.Add(new JsonPatchOperation
|
||||
{
|
||||
Op = "replace",
|
||||
Path = $"/steps/{index}/status",
|
||||
Value = statusValue
|
||||
});
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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.AgenticUI;
|
||||
|
||||
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateAgenticUI")]
|
||||
internal sealed class AgenticUIAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public AgenticUIAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Track function calls that should trigger state events
|
||||
var trackedFunctionCalls = new Dictionary<string, FunctionCallContent>();
|
||||
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Process contents: track function calls and emit state events for results
|
||||
List<AIContent> stateEventsToEmit = new();
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
if (content is FunctionCallContent callContent)
|
||||
{
|
||||
if (callContent.Name == "create_plan" || callContent.Name == "update_plan_step")
|
||||
{
|
||||
trackedFunctionCalls[callContent.CallId] = callContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (content is FunctionResultContent resultContent)
|
||||
{
|
||||
// Check if this result matches a tracked function call
|
||||
if (trackedFunctionCalls.TryGetValue(resultContent.CallId, out var matchedCall))
|
||||
{
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes((JsonElement)resultContent.Result!, this._jsonSerializerOptions);
|
||||
|
||||
// Determine event type based on the function name
|
||||
if (matchedCall.Name == "create_plan")
|
||||
{
|
||||
stateEventsToEmit.Add(new DataContent(bytes, "application/json"));
|
||||
}
|
||||
else if (matchedCall.Name == "update_plan_step")
|
||||
{
|
||||
stateEventsToEmit.Add(new DataContent(bytes, "application/json-patch+json"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield return update;
|
||||
|
||||
yield return new AgentResponseUpdate(
|
||||
new ChatResponseUpdate(role: ChatRole.System, stateEventsToEmit)
|
||||
{
|
||||
MessageId = "delta_" + Guid.NewGuid().ToString("N"),
|
||||
CreatedAt = update.CreatedAt,
|
||||
ResponseId = update.ResponseId,
|
||||
AuthorName = update.AuthorName,
|
||||
Role = update.Role,
|
||||
ContinuationToken = update.ContinuationToken,
|
||||
AdditionalProperties = update.AdditionalProperties,
|
||||
})
|
||||
{
|
||||
AgentId = update.AgentId
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.AgenticUI;
|
||||
|
||||
internal sealed class JsonPatchOperation
|
||||
{
|
||||
[JsonPropertyName("op")]
|
||||
public required string Op { get; set; }
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public required string Path { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public object? Value { get; set; }
|
||||
|
||||
[JsonPropertyName("from")]
|
||||
public string? From { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.AgenticUI;
|
||||
|
||||
internal sealed class Plan
|
||||
{
|
||||
[JsonPropertyName("steps")]
|
||||
public List<Step> Steps { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.AgenticUI;
|
||||
|
||||
internal sealed class Step
|
||||
{
|
||||
[JsonPropertyName("description")]
|
||||
public required string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public StepStatus Status { get; set; } = StepStatus.Pending;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.AgenticUI;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<StepStatus>))]
|
||||
internal enum StepStatus
|
||||
{
|
||||
Pending,
|
||||
Completed
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.BackendToolRendering;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using AGUIDojoServer.AgenticUI;
|
||||
using AGUIDojoServer.BackendToolRendering;
|
||||
using AGUIDojoServer.PredictiveStateUpdates;
|
||||
using AGUIDojoServer.SharedState;
|
||||
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().AsAIAgent(
|
||||
name: "AgenticChat",
|
||||
description: "A simple chat agent using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateBackendToolRendering()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
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().AsAIAgent(
|
||||
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().AsAIAgent(
|
||||
name: "ToolBasedGenerativeUIAgent",
|
||||
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static AIAgent CreateAgenticUI(JsonSerializerOptions options)
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "AgenticUIAgent",
|
||||
Description = "An agent that generates agentic user interfaces using Azure OpenAI",
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
Instructions = """
|
||||
When planning use tools only, without any other messages.
|
||||
IMPORTANT:
|
||||
- Use the `create_plan` tool to set the initial state of the steps
|
||||
- Use the `update_plan_step` tool to update the status of each step
|
||||
- Do NOT repeat the plan or summarise it in a message
|
||||
- Do NOT confirm the creation or updates in a message
|
||||
- Do NOT ask the user for additional information or next steps
|
||||
- Do NOT leave a plan hanging, always complete the plan via `update_plan_step` if one is ongoing.
|
||||
- Continue calling update_plan_step until all steps are marked as completed.
|
||||
|
||||
Only one plan can be active at a time, so do not call the `create_plan` tool
|
||||
again until all the steps in current plan are completed.
|
||||
""",
|
||||
Tools = [
|
||||
AIFunctionFactory.Create(
|
||||
AgenticPlanningTools.CreatePlan,
|
||||
name: "create_plan",
|
||||
description: "Create a plan with multiple steps.",
|
||||
AGUIDojoServerSerializerContext.Default.Options),
|
||||
AIFunctionFactory.Create(
|
||||
AgenticPlanningTools.UpdatePlanStepAsync,
|
||||
name: "update_plan_step",
|
||||
description: "Update a step in the plan with new description or status.",
|
||||
AGUIDojoServerSerializerContext.Default.Options)
|
||||
],
|
||||
AllowMultipleToolCalls = false
|
||||
}
|
||||
});
|
||||
|
||||
return new AgenticUIAgent(baseAgent, options);
|
||||
}
|
||||
|
||||
public static AIAgent CreateSharedState(JsonSerializerOptions options)
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(
|
||||
name: "SharedStateAgent",
|
||||
description: "An agent that demonstrates shared state patterns using Azure OpenAI");
|
||||
|
||||
return new SharedStateAgent(baseAgent, options);
|
||||
}
|
||||
|
||||
public static AIAgent CreatePredictiveStateUpdates(JsonSerializerOptions options)
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "PredictiveStateUpdatesAgent",
|
||||
Description = "An agent that demonstrates predictive state updates using Azure OpenAI",
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
Instructions = """
|
||||
You are a document editor assistant. When asked to write or edit content:
|
||||
|
||||
IMPORTANT:
|
||||
- Use the `write_document` tool with the full document text in Markdown format
|
||||
- Format the document extensively so it's easy to read
|
||||
- You can use all kinds of markdown (headings, lists, bold, etc.)
|
||||
- However, do NOT use italic or strike-through formatting
|
||||
- You MUST write the full document, even when changing only a few words
|
||||
- When making edits to the document, try to make them minimal - do not change every word
|
||||
- Keep stories SHORT!
|
||||
- After you are done writing the document you MUST call a confirm_changes tool after you call write_document
|
||||
|
||||
After the user confirms the changes, provide a brief summary of what you wrote.
|
||||
""",
|
||||
Tools = [
|
||||
AIFunctionFactory.Create(
|
||||
WriteDocument,
|
||||
name: "write_document",
|
||||
description: "Write a document. Use markdown formatting to format the document.",
|
||||
AGUIDojoServerSerializerContext.Default.Options)
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return new PredictiveStateUpdatesAgent(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
|
||||
};
|
||||
|
||||
[Description("Write a document in markdown format.")]
|
||||
private static string WriteDocument([Description("The document content to write.")] string document)
|
||||
{
|
||||
// Simply return success - the document is tracked via state updates
|
||||
return "Document written successfully";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.PredictiveStateUpdates;
|
||||
|
||||
internal sealed class DocumentState
|
||||
{
|
||||
[JsonPropertyName("document")]
|
||||
public string Document { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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.PredictiveStateUpdates;
|
||||
|
||||
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreatePredictiveStateUpdates")]
|
||||
internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
private const int ChunkSize = 10; // Characters per chunk for streaming effect
|
||||
|
||||
public PredictiveStateUpdatesAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Track the last emitted document state to avoid duplicates
|
||||
string? lastEmittedDocument = null;
|
||||
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Check if we're seeing a write_document tool call and emit predictive state
|
||||
bool hasToolCall = false;
|
||||
string? documentContent = null;
|
||||
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
if (content is FunctionCallContent callContent && callContent.Name == "write_document")
|
||||
{
|
||||
hasToolCall = true;
|
||||
// Try to extract the document argument directly from the dictionary
|
||||
if (callContent.Arguments?.TryGetValue("document", out var documentValue) == true)
|
||||
{
|
||||
documentContent = documentValue?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always yield the original update first
|
||||
yield return update;
|
||||
|
||||
// If we got a complete tool call with document content, "fake" stream it in chunks
|
||||
if (hasToolCall && documentContent != null && documentContent != lastEmittedDocument)
|
||||
{
|
||||
// Chunk the document content and emit progressive state updates
|
||||
int startIndex = 0;
|
||||
if (lastEmittedDocument != null && documentContent.StartsWith(lastEmittedDocument, StringComparison.Ordinal))
|
||||
{
|
||||
// Only stream the new portion that was added
|
||||
startIndex = lastEmittedDocument.Length;
|
||||
}
|
||||
|
||||
// Stream the document in chunks
|
||||
for (int i = startIndex; i < documentContent.Length; i += ChunkSize)
|
||||
{
|
||||
int length = Math.Min(ChunkSize, documentContent.Length - i);
|
||||
string chunk = documentContent.Substring(0, i + length);
|
||||
|
||||
// Prepare predictive state update as DataContent
|
||||
var stateUpdate = new DocumentState { Document = chunk };
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateUpdate,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(DocumentState)));
|
||||
|
||||
yield return new AgentResponseUpdate(
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, [new DataContent(stateBytes, "application/json")])
|
||||
{
|
||||
MessageId = "snapshot" + Guid.NewGuid().ToString("N"),
|
||||
CreatedAt = update.CreatedAt,
|
||||
ResponseId = update.ResponseId,
|
||||
AdditionalProperties = update.AdditionalProperties,
|
||||
AuthorName = update.AuthorName,
|
||||
ContinuationToken = update.ContinuationToken,
|
||||
})
|
||||
{
|
||||
AgentId = update.AgentId
|
||||
};
|
||||
|
||||
// Small delay to simulate streaming
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
lastEmittedDocument = documentContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs
Normal file
47
dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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());
|
||||
|
||||
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
|
||||
app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI(jsonOptions.Value.SerializerOptions));
|
||||
|
||||
app.MapAGUI("/shared_state", ChatClientAgentFactory.CreateSharedState(jsonOptions.Value.SerializerOptions));
|
||||
|
||||
app.MapAGUI("/predictive_state_updates", ChatClientAgentFactory.CreatePredictiveStateUpdates(jsonOptions.Value.SerializerOptions));
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
public partial class Program;
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"AGUIDojoServer": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:5018"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.SharedState;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.SharedState;
|
||||
|
||||
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; } = [];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer.SharedState;
|
||||
|
||||
#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();
|
||||
}
|
||||
@@ -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.SharedState;
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
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<AgentResponseUpdate>();
|
||||
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.ToAgentResponse();
|
||||
|
||||
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateSnapshot,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
|
||||
yield return new AgentResponseUpdate
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
22
dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.csproj
Normal file
22
dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</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>
|
||||
17
dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.http
Normal file
17
dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.http
Normal file
@@ -0,0 +1,17 @@
|
||||
@host = http://localhost:5100
|
||||
|
||||
### Send a message to the AG-UI agent
|
||||
POST {{host}}/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"threadId": "thread_123",
|
||||
"runId": "run_456",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the capital of France?"
|
||||
}
|
||||
],
|
||||
"context": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIServer;
|
||||
|
||||
[JsonSerializable(typeof(ServerWeatherForecastRequest))]
|
||||
[JsonSerializable(typeof(ServerWeatherForecastResponse))]
|
||||
internal sealed partial class AGUIServerSerializerContext : JsonSerializerContext;
|
||||
51
dotnet/samples/AGUIClientServer/AGUIServer/Program.cs
Normal file
51
dotnet/samples/AGUIClientServer/AGUIServer/Program.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using AGUIServer;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIServerSerializerContext.Default));
|
||||
builder.Services.AddAGUI();
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Create the AI agent with tools
|
||||
var agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
tools: [
|
||||
AIFunctionFactory.Create(
|
||||
() => DateTimeOffset.UtcNow,
|
||||
name: "get_current_time",
|
||||
description: "Get the current UTC time."
|
||||
),
|
||||
AIFunctionFactory.Create(
|
||||
([Description("The weather forecast request")]ServerWeatherForecastRequest request) => {
|
||||
return new ServerWeatherForecastResponse()
|
||||
{
|
||||
Summary = "Sunny",
|
||||
TemperatureC = 25,
|
||||
Date = request.Date
|
||||
};
|
||||
},
|
||||
name: "get_server_weather_forecast",
|
||||
description: "Gets the forecast for a specific location and date",
|
||||
AGUIServerSerializerContext.Default.Options)
|
||||
]);
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUI("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"AGUIServer": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:5100;https://localhost:5101"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AGUIServer;
|
||||
|
||||
internal sealed class ServerWeatherForecastRequest
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public string Location { get; set; } = "Seattle";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AGUIServer;
|
||||
|
||||
internal sealed class ServerWeatherForecastResponse
|
||||
{
|
||||
public string Summary { get; set; } = "";
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
208
dotnet/samples/AGUIClientServer/README.md
Normal file
208
dotnet/samples/AGUIClientServer/README.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# AG-UI Client and Server Sample
|
||||
|
||||
This sample demonstrates how to use the AG-UI (Agent UI) protocol to enable communication between a client application and a remote agent server. The AG-UI protocol provides a standardized way for clients to interact with AI agents.
|
||||
|
||||
## Overview
|
||||
|
||||
The demonstration has two components:
|
||||
|
||||
1. **AGUIServer** - An ASP.NET Core web server that hosts an AI agent and exposes it via the AG-UI protocol
|
||||
2. **AGUIClient** - A console application that connects to the AG-UI server and displays streaming updates
|
||||
|
||||
> **Warning**
|
||||
> The AG-UI protocol is still under development and changing.
|
||||
> We will try to keep these samples updated as the protocol evolves.
|
||||
|
||||
## Configuring Environment Variables
|
||||
|
||||
Configure the required Azure OpenAI environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="<<your-model-endpoint>>"
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4.1-mini"
|
||||
```
|
||||
|
||||
> **Note:** This sample uses `DefaultAzureCredential` for authentication. Make sure you're authenticated with Azure (e.g., via `az login`, Visual Studio, or environment variables).
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Step 1: Start the AG-UI Server
|
||||
|
||||
```bash
|
||||
cd AGUIServer
|
||||
dotnet build
|
||||
dotnet run --urls "http://localhost:5100"
|
||||
```
|
||||
|
||||
The server will start and listen on `http://localhost:5100`.
|
||||
|
||||
### Step 2: Testing with the REST Client (Optional)
|
||||
|
||||
Before running the client, you can test the server using the included `.http` file:
|
||||
|
||||
1. Open [./AGUIServer/AGUIServer.http](./AGUIServer/AGUIServer.http) in Visual Studio or VS Code with the REST Client extension
|
||||
2. Send a test request to verify the server is working
|
||||
3. Observe the server-sent events stream in the response
|
||||
|
||||
Sample request:
|
||||
```http
|
||||
POST http://localhost:5100/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"threadId": "thread_123",
|
||||
"runId": "run_456",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the capital of France?"
|
||||
}
|
||||
],
|
||||
"context": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Run the AG-UI Client
|
||||
|
||||
In a new terminal window:
|
||||
|
||||
```bash
|
||||
cd AGUIClient
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Optionally, configure a different server URL:
|
||||
|
||||
```powershell
|
||||
$env:AGUI_SERVER_URL="http://localhost:5100"
|
||||
```
|
||||
|
||||
### Step 4: Interact with the Agent
|
||||
|
||||
1. The client will connect to the AG-UI server
|
||||
2. Enter your message at the prompt
|
||||
3. Observe the streaming updates with color-coded output:
|
||||
- **Yellow**: Run started notification showing thread and run IDs
|
||||
- **Cyan**: Agent's text response (streamed character by character)
|
||||
- **Green**: Run finished notification
|
||||
- **Red**: Error messages (if any occur)
|
||||
4. Type `:q` or `quit` to exit
|
||||
|
||||
## Sample Output
|
||||
|
||||
```
|
||||
AGUIClient> dotnet run
|
||||
info: AGUIClient[0]
|
||||
Connecting to AG-UI server at: http://localhost:5100
|
||||
|
||||
User (:q or quit to exit): What is the capital of France?
|
||||
|
||||
[Run Started - Thread: thread_abc123, Run: run_xyz789]
|
||||
The capital of France is Paris. It is known for its rich history, culture, and iconic landmarks such as the Eiffel Tower and the Louvre Museum.
|
||||
[Run Finished - Thread: thread_abc123, Run: run_xyz789]
|
||||
|
||||
User (:q or quit to exit): Tell me a fun fact about space
|
||||
|
||||
[Run Started - Thread: thread_abc123, Run: run_def456]
|
||||
Here's a fun fact: A day on Venus is longer than its year! Venus takes about 243 Earth days to rotate once on its axis, but only about 225 Earth days to orbit the Sun.
|
||||
[Run Finished - Thread: thread_abc123, Run: run_def456]
|
||||
|
||||
User (:q or quit to exit): :q
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Server Side
|
||||
|
||||
The `AGUIServer` uses the `MapAGUI` extension method to expose an agent through the AG-UI protocol:
|
||||
|
||||
```csharp
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetChatClient(model)
|
||||
.AsAIAgent(
|
||||
instructions: "You are a helpful assistant.",
|
||||
name: "AGUIAssistant");
|
||||
|
||||
app.MapAGUI("/", agent);
|
||||
```
|
||||
|
||||
This automatically handles:
|
||||
- HTTP POST requests with message payloads
|
||||
- Converting agent responses to AG-UI event streams
|
||||
- Server-sent events (SSE) formatting
|
||||
- Thread and run management
|
||||
|
||||
### Client Side
|
||||
|
||||
The `AGUIClient` uses the `AGUIChatClient` to connect to the remote server:
|
||||
|
||||
```csharp
|
||||
using HttpClient httpClient = new();
|
||||
var chatClient = new AGUIChatClient(
|
||||
httpClient,
|
||||
endpoint: serverUrl,
|
||||
modelId: "agui-client",
|
||||
jsonSerializerOptions: null);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
instructions: null,
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent",
|
||||
tools: []);
|
||||
|
||||
bool isFirstUpdate = true;
|
||||
AgentResponseUpdate? currentUpdate = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
|
||||
{
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
Console.WriteLine($"[Run Started - Thread: {update.ConversationId}, Run: {update.ResponseId}]");
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
|
||||
currentUpdate = update;
|
||||
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
// Display streaming text
|
||||
Console.Write(textContent.Text);
|
||||
break;
|
||||
case ErrorContent errorContent:
|
||||
// Display error notification
|
||||
Console.WriteLine($"[Error: {errorContent.Message}]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last update indicates run finished
|
||||
if (currentUpdate != null)
|
||||
{
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {currentUpdate.ConversationId}, Run: {currentUpdate.ResponseId}]");
|
||||
}
|
||||
```
|
||||
|
||||
The `RunStreamingAsync` method:
|
||||
1. Sends messages to the server via HTTP POST
|
||||
2. Receives server-sent events (SSE) stream
|
||||
3. Parses events into `AgentResponseUpdate` objects
|
||||
4. Yields updates as they arrive for real-time display
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Thread**: Represents a conversation context that persists across multiple runs (accessed via `ConversationId` property)
|
||||
- **Run**: A single execution of the agent for a given set of messages (identified by `ResponseId` property)
|
||||
- **AgentResponseUpdate**: Contains the response data with:
|
||||
- `ResponseId`: The unique run identifier
|
||||
- `ConversationId`: The thread/conversation identifier
|
||||
- `Contents`: Collection of content items (TextContent, ErrorContent, etc.)
|
||||
- **Run Lifecycle**:
|
||||
- The **first** `AgentResponseUpdate` in a run indicates the run has started
|
||||
- Subsequent updates contain streaming content as the agent processes
|
||||
- The **last** `AgentResponseUpdate` in a run indicates the run has finished
|
||||
- If an error occurs, the update will contain `ErrorContent`
|
||||
Reference in New Issue
Block a user