Skip to content

Commit eb06fae

Browse files
authored
.NET: AG-UI Docs samples (#2194)
* Add AG-UI Blazor sample * Add AG-UI getting started samples * Cleanups * Update the dojo samples * cleanups * Fix readme * Address feedback and further cleanups * Fix build * Missing fixes
1 parent 8c6b12e commit eb06fae

File tree

90 files changed

+4228
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+4228
-12
lines changed

dotnet/agent-framework-dotnet.slnx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,29 @@
8484
<Folder Name="/Samples/GettingStarted/DeclarativeAgents/">
8585
<Project Path="samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
8686
</Folder>
87+
<Folder Name="/Samples/GettingStarted/AGUI/">
88+
<File Path="samples/GettingStarted/AGUI/README.md" />
89+
</Folder>
90+
<Folder Name="/Samples/GettingStarted/AGUI/Step01_GettingStarted/">
91+
<Project Path="samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj" />
92+
<Project Path="samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj" />
93+
</Folder>
94+
<Folder Name="/Samples/GettingStarted/AGUI/Step02_BackendTools/">
95+
<Project Path="samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj" />
96+
<Project Path="samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj" />
97+
</Folder>
98+
<Folder Name="/Samples/GettingStarted/AGUI/Step03_FrontendTools/">
99+
<Project Path="samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj" />
100+
<Project Path="samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj" />
101+
</Folder>
102+
<Folder Name="/Samples/GettingStarted/AGUI/Step04_HumanInLoop/">
103+
<Project Path="samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj" />
104+
<Project Path="samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj" />
105+
</Folder>
106+
<Folder Name="/Samples/GettingStarted/AGUI/Step05_StateManagement/">
107+
<Project Path="samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj" />
108+
<Project Path="samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj" />
109+
</Folder>
87110
<Folder Name="/Samples/GettingStarted/DevUI/">
88111
<File Path="samples/GettingStarted/DevUI/README.md" />
89112
<Project Path="samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System.Text.Json.Serialization;
4+
using AGUIDojoServer.AgenticUI;
5+
using AGUIDojoServer.BackendToolRendering;
6+
using AGUIDojoServer.PredictiveStateUpdates;
7+
using AGUIDojoServer.SharedState;
48

59
namespace AGUIDojoServer;
610

711
[JsonSerializable(typeof(WeatherInfo))]
812
[JsonSerializable(typeof(Recipe))]
913
[JsonSerializable(typeof(Ingredient))]
1014
[JsonSerializable(typeof(RecipeResponse))]
15+
[JsonSerializable(typeof(Plan))]
16+
[JsonSerializable(typeof(Step))]
17+
[JsonSerializable(typeof(StepStatus))]
18+
[JsonSerializable(typeof(StepStatus?))]
19+
[JsonSerializable(typeof(JsonPatchOperation))]
20+
[JsonSerializable(typeof(List<JsonPatchOperation>))]
21+
[JsonSerializable(typeof(List<string>))]
22+
[JsonSerializable(typeof(DocumentState))]
1123
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.ComponentModel;
4+
5+
namespace AGUIDojoServer.AgenticUI;
6+
7+
internal static class AgenticPlanningTools
8+
{
9+
[Description("Create a plan with multiple steps.")]
10+
public static Plan CreatePlan([Description("List of step descriptions to create the plan.")] List<string> steps)
11+
{
12+
return new Plan
13+
{
14+
Steps = [.. steps.Select(s => new Step { Description = s, Status = StepStatus.Pending })]
15+
};
16+
}
17+
18+
[Description("Update a step in the plan with new description or status.")]
19+
public static async Task<List<JsonPatchOperation>> UpdatePlanStepAsync(
20+
[Description("The index of the step to update.")] int index,
21+
[Description("The new description for the step (optional).")] string? description = null,
22+
[Description("The new status for the step (optional).")] StepStatus? status = null)
23+
{
24+
var changes = new List<JsonPatchOperation>();
25+
26+
if (description is not null)
27+
{
28+
changes.Add(new JsonPatchOperation
29+
{
30+
Op = "replace",
31+
Path = $"/steps/{index}/description",
32+
Value = description
33+
});
34+
}
35+
36+
if (status.HasValue)
37+
{
38+
// Status must be lowercase to match AG-UI frontend expectations: "pending" or "completed"
39+
string statusValue = status.Value == StepStatus.Pending ? "pending" : "completed";
40+
changes.Add(new JsonPatchOperation
41+
{
42+
Op = "replace",
43+
Path = $"/steps/{index}/status",
44+
Value = statusValue
45+
});
46+
}
47+
48+
await Task.Delay(1000);
49+
50+
return changes;
51+
}
52+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Runtime.CompilerServices;
5+
using System.Text.Json;
6+
using Microsoft.Agents.AI;
7+
using Microsoft.Extensions.AI;
8+
9+
namespace AGUIDojoServer.AgenticUI;
10+
11+
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateAgenticUI")]
12+
internal sealed class AgenticUIAgent : DelegatingAIAgent
13+
{
14+
private readonly JsonSerializerOptions _jsonSerializerOptions;
15+
16+
public AgenticUIAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
17+
: base(innerAgent)
18+
{
19+
this._jsonSerializerOptions = jsonSerializerOptions;
20+
}
21+
22+
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
23+
{
24+
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
25+
}
26+
27+
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
28+
IEnumerable<ChatMessage> messages,
29+
AgentThread? thread = null,
30+
AgentRunOptions? options = null,
31+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
32+
{
33+
// Track function calls that should trigger state events
34+
var trackedFunctionCalls = new Dictionary<string, FunctionCallContent>();
35+
36+
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
37+
{
38+
// Process contents: track function calls and emit state events for results
39+
List<AIContent> stateEventsToEmit = new();
40+
foreach (var content in update.Contents)
41+
{
42+
if (content is FunctionCallContent callContent)
43+
{
44+
if (callContent.Name == "create_plan" || callContent.Name == "update_plan_step")
45+
{
46+
trackedFunctionCalls[callContent.CallId] = callContent;
47+
break;
48+
}
49+
}
50+
else if (content is FunctionResultContent resultContent)
51+
{
52+
// Check if this result matches a tracked function call
53+
if (trackedFunctionCalls.TryGetValue(resultContent.CallId, out var matchedCall))
54+
{
55+
var bytes = JsonSerializer.SerializeToUtf8Bytes((JsonElement)resultContent.Result!, this._jsonSerializerOptions);
56+
57+
// Determine event type based on the function name
58+
if (matchedCall.Name == "create_plan")
59+
{
60+
stateEventsToEmit.Add(new DataContent(bytes, "application/json"));
61+
}
62+
else if (matchedCall.Name == "update_plan_step")
63+
{
64+
stateEventsToEmit.Add(new DataContent(bytes, "application/json-patch+json"));
65+
}
66+
}
67+
}
68+
}
69+
70+
yield return update;
71+
72+
yield return new AgentRunResponseUpdate(
73+
new ChatResponseUpdate(role: ChatRole.System, stateEventsToEmit)
74+
{
75+
MessageId = "delta_" + Guid.NewGuid().ToString("N"),
76+
CreatedAt = update.CreatedAt,
77+
ResponseId = update.ResponseId,
78+
AuthorName = update.AuthorName,
79+
Role = update.Role,
80+
ContinuationToken = update.ContinuationToken,
81+
AdditionalProperties = update.AdditionalProperties,
82+
})
83+
{
84+
AgentId = update.AgentId
85+
};
86+
}
87+
}
88+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json.Serialization;
4+
5+
namespace AGUIDojoServer.AgenticUI;
6+
7+
internal sealed class JsonPatchOperation
8+
{
9+
[JsonPropertyName("op")]
10+
public required string Op { get; set; }
11+
12+
[JsonPropertyName("path")]
13+
public required string Path { get; set; }
14+
15+
[JsonPropertyName("value")]
16+
public object? Value { get; set; }
17+
18+
[JsonPropertyName("from")]
19+
public string? From { get; set; }
20+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json.Serialization;
4+
5+
namespace AGUIDojoServer.AgenticUI;
6+
7+
internal sealed class Plan
8+
{
9+
[JsonPropertyName("steps")]
10+
public List<Step> Steps { get; set; } = [];
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json.Serialization;
4+
5+
namespace AGUIDojoServer.AgenticUI;
6+
7+
internal sealed class Step
8+
{
9+
[JsonPropertyName("description")]
10+
public required string Description { get; set; }
11+
12+
[JsonPropertyName("status")]
13+
public StepStatus Status { get; set; } = StepStatus.Pending;
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json.Serialization;
4+
5+
namespace AGUIDojoServer.AgenticUI;
6+
7+
[JsonConverter(typeof(JsonStringEnumConverter<StepStatus>))]
8+
internal enum StepStatus
9+
{
10+
Pending,
11+
Completed
12+
}

dotnet/samples/AGUIClientServer/AGUIDojoServer/WeatherInfo.cs renamed to dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
using System.Text.Json.Serialization;
44

5-
namespace AGUIDojoServer;
5+
namespace AGUIDojoServer.BackendToolRendering;
66

77
internal sealed class WeatherInfo
88
{

dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22

33
using System.ComponentModel;
44
using System.Text.Json;
5+
using AGUIDojoServer.AgenticUI;
6+
using AGUIDojoServer.BackendToolRendering;
7+
using AGUIDojoServer.PredictiveStateUpdates;
8+
using AGUIDojoServer.SharedState;
59
using Azure.AI.OpenAI;
610
using Azure.Identity;
711
using Microsoft.Agents.AI;
812
using Microsoft.Extensions.AI;
13+
using OpenAI;
914
using ChatClient = OpenAI.Chat.ChatClient;
1015

1116
namespace AGUIDojoServer;
@@ -66,13 +71,46 @@ public static ChatClientAgent CreateToolBasedGenerativeUI()
6671
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
6772
}
6873

69-
public static ChatClientAgent CreateAgenticUI()
74+
public static AIAgent CreateAgenticUI(JsonSerializerOptions options)
7075
{
7176
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
72-
73-
return chatClient.AsIChatClient().CreateAIAgent(
74-
name: "AgenticUIAgent",
75-
description: "An agent that generates agentic user interfaces using Azure OpenAI");
77+
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(new ChatClientAgentOptions
78+
{
79+
Name = "AgenticUIAgent",
80+
Description = "An agent that generates agentic user interfaces using Azure OpenAI",
81+
ChatOptions = new ChatOptions
82+
{
83+
Instructions = """
84+
When planning use tools only, without any other messages.
85+
IMPORTANT:
86+
- Use the `create_plan` tool to set the initial state of the steps
87+
- Use the `update_plan_step` tool to update the status of each step
88+
- Do NOT repeat the plan or summarise it in a message
89+
- Do NOT confirm the creation or updates in a message
90+
- Do NOT ask the user for additional information or next steps
91+
- Do NOT leave a plan hanging, always complete the plan via `update_plan_step` if one is ongoing.
92+
- Continue calling update_plan_step until all steps are marked as completed.
93+
94+
Only one plan can be active at a time, so do not call the `create_plan` tool
95+
again until all the steps in current plan are completed.
96+
""",
97+
Tools = [
98+
AIFunctionFactory.Create(
99+
AgenticPlanningTools.CreatePlan,
100+
name: "create_plan",
101+
description: "Create a plan with multiple steps.",
102+
AGUIDojoServerSerializerContext.Default.Options),
103+
AIFunctionFactory.Create(
104+
AgenticPlanningTools.UpdatePlanStepAsync,
105+
name: "update_plan_step",
106+
description: "Update a step in the plan with new description or status.",
107+
AGUIDojoServerSerializerContext.Default.Options)
108+
],
109+
AllowMultipleToolCalls = false
110+
}
111+
});
112+
113+
return new AgenticUIAgent(baseAgent, options);
76114
}
77115

78116
public static AIAgent CreateSharedState(JsonSerializerOptions options)
@@ -86,6 +124,44 @@ public static AIAgent CreateSharedState(JsonSerializerOptions options)
86124
return new SharedStateAgent(baseAgent, options);
87125
}
88126

127+
public static AIAgent CreatePredictiveStateUpdates(JsonSerializerOptions options)
128+
{
129+
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
130+
131+
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(new ChatClientAgentOptions
132+
{
133+
Name = "PredictiveStateUpdatesAgent",
134+
Description = "An agent that demonstrates predictive state updates using Azure OpenAI",
135+
ChatOptions = new ChatOptions
136+
{
137+
Instructions = """
138+
You are a document editor assistant. When asked to write or edit content:
139+
140+
IMPORTANT:
141+
- Use the `write_document` tool with the full document text in Markdown format
142+
- Format the document extensively so it's easy to read
143+
- You can use all kinds of markdown (headings, lists, bold, etc.)
144+
- However, do NOT use italic or strike-through formatting
145+
- You MUST write the full document, even when changing only a few words
146+
- When making edits to the document, try to make them minimal - do not change every word
147+
- Keep stories SHORT!
148+
- After you are done writing the document you MUST call a confirm_changes tool after you call write_document
149+
150+
After the user confirms the changes, provide a brief summary of what you wrote.
151+
""",
152+
Tools = [
153+
AIFunctionFactory.Create(
154+
WriteDocument,
155+
name: "write_document",
156+
description: "Write a document. Use markdown formatting to format the document.",
157+
AGUIDojoServerSerializerContext.Default.Options)
158+
]
159+
}
160+
});
161+
162+
return new PredictiveStateUpdatesAgent(baseAgent, options);
163+
}
164+
89165
[Description("Get the weather for a given location.")]
90166
private static WeatherInfo GetWeather([Description("The location to get the weather for.")] string location) => new()
91167
{
@@ -95,4 +171,11 @@ public static AIAgent CreateSharedState(JsonSerializerOptions options)
95171
WindSpeed = 10,
96172
FeelsLike = 25
97173
};
174+
175+
[Description("Write a document in markdown format.")]
176+
private static string WriteDocument([Description("The document content to write.")] string document)
177+
{
178+
// Simply return success - the document is tracked via state updates
179+
return "Document written successfully";
180+
}
98181
}

0 commit comments

Comments
 (0)