From 077663ab1f10d2c95baf42c9c299f5f1bc556e38 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 13 Nov 2025 20:08:34 +0100 Subject: [PATCH 1/9] Add AG-UI Blazor sample --- .../Client/AGUIWebChatClient.csproj | 19 ++ .../AGUIWebChat/Client/Components/App.razor | 23 +++ .../Components/Layout/LoadingSpinner.razor | 1 + .../Layout/LoadingSpinner.razor.css | 89 +++++++++ .../Client/Components/Layout/MainLayout.razor | 9 + .../Components/Layout/MainLayout.razor.css | 20 ++ .../Client/Components/Pages/Chat/Chat.razor | 94 +++++++++ .../Components/Pages/Chat/Chat.razor.css | 11 ++ .../Components/Pages/Chat/ChatCitation.razor | 38 ++++ .../Pages/Chat/ChatCitation.razor.css | 37 ++++ .../Components/Pages/Chat/ChatHeader.razor | 17 ++ .../Pages/Chat/ChatHeader.razor.css | 25 +++ .../Components/Pages/Chat/ChatInput.razor | 51 +++++ .../Components/Pages/Chat/ChatInput.razor.css | 57 ++++++ .../Components/Pages/Chat/ChatInput.razor.js | 43 ++++ .../Pages/Chat/ChatMessageItem.razor | 73 +++++++ .../Pages/Chat/ChatMessageItem.razor.css | 67 +++++++ .../Pages/Chat/ChatMessageList.razor | 42 ++++ .../Pages/Chat/ChatMessageList.razor.css | 22 +++ .../Pages/Chat/ChatMessageList.razor.js | 34 ++++ .../Pages/Chat/ChatSuggestions.razor | 78 ++++++++ .../Pages/Chat/ChatSuggestions.razor.css | 9 + .../Client/Components/Routes.razor | 6 + .../Client/Components/_Imports.razor | 12 ++ dotnet/samples/AGUIWebChat/Client/Program.cs | 32 +++ .../Client/Properties/launchSettings.json | 15 ++ .../AGUIWebChat/Client/wwwroot/app.css | 93 +++++++++ .../AGUIWebChat/Client/wwwroot/favicon.png | Bin 0 -> 1148 bytes dotnet/samples/AGUIWebChat/README.md | 185 ++++++++++++++++++ .../Server/AGUIWebChatServer.csproj | 23 +++ dotnet/samples/AGUIWebChat/Server/Program.cs | 35 ++++ .../Server/Properties/launchSettings.json | 14 ++ 32 files changed, 1274 insertions(+) create mode 100644 dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/App.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.js create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.js create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor.css create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/Routes.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Components/_Imports.razor create mode 100644 dotnet/samples/AGUIWebChat/Client/Program.cs create mode 100644 dotnet/samples/AGUIWebChat/Client/Properties/launchSettings.json create mode 100644 dotnet/samples/AGUIWebChat/Client/wwwroot/app.css create mode 100644 dotnet/samples/AGUIWebChat/Client/wwwroot/favicon.png create mode 100644 dotnet/samples/AGUIWebChat/README.md create mode 100644 dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj create mode 100644 dotnet/samples/AGUIWebChat/Server/Program.cs create mode 100644 dotnet/samples/AGUIWebChat/Server/Properties/launchSettings.json diff --git a/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj b/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj new file mode 100644 index 0000000000..a1fe8b8f34 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + diff --git a/dotnet/samples/AGUIWebChat/Client/Components/App.razor b/dotnet/samples/AGUIWebChat/Client/Components/App.razor new file mode 100644 index 0000000000..a64d576883 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/App.razor @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + +@code { + private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false); +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor b/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor new file mode 100644 index 0000000000..116455ce45 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor @@ -0,0 +1 @@ +
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor.css new file mode 100644 index 0000000000..e599d27e86 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor.css @@ -0,0 +1,89 @@ +/* Used under CC0 license */ + +.lds-ellipsis { + color: #666; + animation: fade-in 1s; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + + .lds-ellipsis, + .lds-ellipsis div { + box-sizing: border-box; + } + +.lds-ellipsis { + margin: auto; + display: block; + position: relative; + width: 80px; + height: 80px; +} + + .lds-ellipsis div { + position: absolute; + top: 33.33333px; + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + animation-timing-function: cubic-bezier(0, 1, 1, 0); + } + + .lds-ellipsis div:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; + } + + .lds-ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; + } + + .lds-ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; + } + + .lds-ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; + } + +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + + 100% { + transform: scale(1); + } +} + +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + + 100% { + transform: scale(0); + } +} + +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + + 100% { + transform: translate(24px, 0); + } +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor b/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor new file mode 100644 index 0000000000..f3da3cbae5 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +@Body + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000000..60cec92d5e --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor.css @@ -0,0 +1,20 @@ +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor new file mode 100644 index 0000000000..31eb7e406c --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor @@ -0,0 +1,94 @@ +@page "/" +@using System.ComponentModel +@inject IChatClient ChatClient +@inject NavigationManager Nav +@implements IDisposable + +Chat + + + + + +
Ask the assistant a question to start a conversation.
+
+
+
+ + +
+ +@code { + private const string SystemPrompt = @" + You are a helpful assistant. + "; + + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + StateHasChanged(); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + } + + public void Dispose() + => currentResponseCancellation?.Cancel(); +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor.css new file mode 100644 index 0000000000..08841605f6 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor.css @@ -0,0 +1,11 @@ +.chat-container { + position: sticky; + bottom: 0; + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 0.75rem; + padding-bottom: 1.5rem; + border-top-width: 1px; + background-color: #F3F4F6; + border-color: #E5E7EB; +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor new file mode 100644 index 0000000000..ccb5853cec --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor @@ -0,0 +1,38 @@ +@using System.Web +@if (!string.IsNullOrWhiteSpace(viewerUrl)) +{ + + + + +
+
@File
+
@Quote
+
+
+} + +@code { + [Parameter] + public required string File { get; set; } + + [Parameter] + public int? PageNumber { get; set; } + + [Parameter] + public required string Quote { get; set; } + + private string? viewerUrl; + + protected override void OnParametersSet() + { + viewerUrl = null; + + // If you ingest other types of content besides PDF files, construct a URL to an appropriate viewer here + if (File.EndsWith(".pdf")) + { + var search = Quote?.Trim('.', ',', ' ', '\n', '\r', '\t', '"', '\''); + viewerUrl = $"lib/pdf_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#page={PageNumber}&search={HttpUtility.UrlEncode(search)}&phrase=true"; + } + } +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor.css new file mode 100644 index 0000000000..763c82aec4 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor.css @@ -0,0 +1,37 @@ +.citation { + display: inline-flex; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + margin-top: 1rem; + margin-right: 1rem; + border-bottom: 2px solid #a770de; + gap: 0.5rem; + border-radius: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + background-color: #ffffff; +} + + .citation[href]:hover { + outline: 1px solid #865cb1; + } + + .citation svg { + width: 1.5rem; + height: 1.5rem; + } + + .citation:active { + background-color: rgba(0,0,0,0.05); + } + +.citation-content { + display: flex; + flex-direction: column; +} + +.citation-file { + font-weight: 600; +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor new file mode 100644 index 0000000000..a339038e2a --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor @@ -0,0 +1,17 @@ +
+
+ +
+ +

AGUI WebChat

+
+ +@code { + [Parameter] + public EventCallback OnNewChat { get; set; } +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor.css new file mode 100644 index 0000000000..97f0a8d43a --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor.css @@ -0,0 +1,25 @@ +.chat-header-container { + top: 0; + padding: 1.5rem; +} + +.chat-header-controls { + margin-bottom: 1.5rem; +} + +h1 { + overflow: hidden; + text-overflow: ellipsis; +} + +.new-chat-icon { + width: 1.25rem; + height: 1.25rem; + color: rgb(55, 65, 81); +} + +@media (min-width: 768px) { + .chat-header-container { + position: sticky; + } +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor new file mode 100644 index 0000000000..e87ac6ccf4 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor @@ -0,0 +1,51 @@ +@inject IJSRuntime JS + + + + + +@code { + private ElementReference textArea; + private string? messageText; + + [Parameter] + public EventCallback OnSend { get; set; } + + public ValueTask FocusAsync() + => textArea.FocusAsync(); + + private async Task SendMessageAsync() + { + if (messageText is { Length: > 0 } text) + { + messageText = null; + await OnSend.InvokeAsync(new ChatMessage(ChatRole.User, text)); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + var module = await JS.InvokeAsync("import", "./Components/Pages/Chat/ChatInput.razor.js"); + await module.InvokeVoidAsync("init", textArea); + await module.DisposeAsync(); + } + catch (JSDisconnectedException) + { + } + } + } +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.css new file mode 100644 index 0000000000..375dd711d9 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.css @@ -0,0 +1,57 @@ +.input-box { + display: flex; + flex-direction: column; + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 8px; + padding: 0.5rem 0.75rem; + margin-top: 0.75rem; +} + + .input-box:focus-within { + outline: 2px solid #4152d5; + } + +textarea { + resize: none; + border: none; + outline: none; + flex-grow: 1; +} + + textarea:placeholder-shown + .tools { + --send-button-color: #aaa; + } + +.tools { + display: flex; + margin-top: 1rem; + align-items: center; +} + +.tool-icon { + width: 1.25rem; + height: 1.25rem; +} + +.send-button { + color: var(--send-button-color); + margin-left: auto; +} + + .send-button:hover { + color: black; + } + +.attach { + background-color: white; + border-style: dashed; + color: #888; + border-color: #888; + padding: 3px 8px; +} + + .attach:hover { + background-color: #f0f0f0; + color: black; + } diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.js b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.js new file mode 100644 index 0000000000..e4bd8af20a --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.js @@ -0,0 +1,43 @@ +export function init(elem) { + elem.focus(); + + // Auto-resize whenever the user types or if the value is set programmatically + elem.addEventListener('input', () => resizeToFit(elem)); + afterPropertyWritten(elem, 'value', () => resizeToFit(elem)); + + // Auto-submit the form on 'enter' keypress + elem.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + elem.dispatchEvent(new CustomEvent('change', { bubbles: true })); + elem.closest('form').dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true })); + } + }); +} + +function resizeToFit(elem) { + const lineHeight = parseFloat(getComputedStyle(elem).lineHeight); + + elem.rows = 1; + const numLines = Math.ceil(elem.scrollHeight / lineHeight); + elem.rows = Math.min(5, Math.max(1, numLines)); +} + +function afterPropertyWritten(target, propName, callback) { + const descriptor = getPropertyDescriptor(target, propName); + Object.defineProperty(target, propName, { + get: function () { + return descriptor.get.apply(this, arguments); + }, + set: function () { + const result = descriptor.set.apply(this, arguments); + callback(); + return result; + } + }); +} + +function getPropertyDescriptor(target, propertyName) { + return Object.getOwnPropertyDescriptor(target, propertyName) + || getPropertyDescriptor(Object.getPrototypeOf(target), propertyName); +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor new file mode 100644 index 0000000000..6f4e1357c9 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor @@ -0,0 +1,73 @@ +@using System.Runtime.CompilerServices +@using System.Text.RegularExpressions +@using System.Linq + +@if (Message.Role == ChatRole.User) +{ +
+ @Message.Text +
+} +else if (Message.Role == ChatRole.Assistant) +{ + foreach (var content in Message.Contents) + { + if (content is TextContent { Text: { Length: > 0 } text }) + { +
+
+
+ + + +
+
+
Assistant
+
+
@((MarkupString)text)
+
+
+ } + else if (content is FunctionCallContent { Name: "Search" } fcc && fcc.Arguments?.TryGetValue("searchPhrase", out var searchPhrase) is true) + { + + } + } +} + +@code { + private static readonly ConditionalWeakTable SubscribersLookup = new(); + + [Parameter, EditorRequired] + public required ChatMessage Message { get; set; } + + [Parameter] + public bool InProgress { get; set;} + + protected override void OnInitialized() + { + SubscribersLookup.AddOrUpdate(Message, this); + } + + public static void NotifyChanged(ChatMessage source) + { + if (SubscribersLookup.TryGetValue(source, out var subscriber)) + { + subscriber.StateHasChanged(); + } + } +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor.css new file mode 100644 index 0000000000..16443cf657 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor.css @@ -0,0 +1,67 @@ +.user-message { + background: rgb(182 215 232); + align-self: flex-end; + min-width: 25%; + max-width: calc(100% - 5rem); + padding: 0.5rem 1.25rem; + border-radius: 0.25rem; + color: #1F2937; + white-space: pre-wrap; +} + +.assistant-message, .assistant-search { + display: grid; + grid-template-rows: min-content; + grid-template-columns: 2rem minmax(0, 1fr); + gap: 0.25rem; +} + +.assistant-message-header { + font-weight: 600; +} + +.assistant-message-text { + grid-column-start: 2; +} + +.assistant-message-icon { + display: flex; + justify-content: center; + align-items: center; + border-radius: 9999px; + width: 1.5rem; + height: 1.5rem; + color: #ffffff; + background: #9b72ce; +} + + .assistant-message-icon svg { + width: 1rem; + height: 1rem; + } + +.assistant-search { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.assistant-search-icon { + display: flex; + justify-content: center; + align-items: center; + width: 1.5rem; + height: 1.5rem; +} + + .assistant-search-icon svg { + width: 1rem; + height: 1rem; + } + +.assistant-search-content { + align-content: center; +} + +.assistant-search-phrase { + font-weight: 600; +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor new file mode 100644 index 0000000000..d245f455f1 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor @@ -0,0 +1,42 @@ +@inject IJSRuntime JS + +
+ + @foreach (var message in Messages) + { + + } + + @if (InProgressMessage is not null) + { + + + } + else if (IsEmpty) + { +
@NoMessagesContent
+ } +
+
+ +@code { + [Parameter] + public required IEnumerable Messages { get; set; } + + [Parameter] + public ChatMessage? InProgressMessage { get; set; } + + [Parameter] + public RenderFragment? NoMessagesContent { get; set; } + + private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text)); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Activates the auto-scrolling behavior + await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js"); + } + } +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.css new file mode 100644 index 0000000000..4be50ddfc3 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.css @@ -0,0 +1,22 @@ +.message-list-container { + margin: 2rem 1.5rem; + flex-grow: 1; +} + +.message-list { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.no-messages { + text-align: center; + font-size: 1.25rem; + color: #999; + margin-top: calc(40vh - 18rem); +} + +chat-messages > ::deep div:last-of-type { + /* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */ + margin-bottom: 2rem; +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.js b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.js new file mode 100644 index 0000000000..9755d47c29 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.js @@ -0,0 +1,34 @@ +// The following logic provides auto-scroll behavior for the chat messages list. +// If you don't want that behavior, you can simply not load this module. + +window.customElements.define('chat-messages', class ChatMessages extends HTMLElement { + static _isFirstAutoScroll = true; + + connectedCallback() { + this._observer = new MutationObserver(mutations => this._scheduleAutoScroll(mutations)); + this._observer.observe(this, { childList: true, attributes: true }); + } + + disconnectedCallback() { + this._observer.disconnect(); + } + + _scheduleAutoScroll(mutations) { + // Debounce the calls in case multiple DOM updates occur together + cancelAnimationFrame(this._nextAutoScroll); + this._nextAutoScroll = requestAnimationFrame(() => { + const addedUserMessage = mutations.some(m => Array.from(m.addedNodes).some(n => n.parentElement === this && n.classList?.contains('user-message'))); + const elem = this.lastElementChild; + if (ChatMessages._isFirstAutoScroll || addedUserMessage || this._elemIsNearScrollBoundary(elem, 300)) { + elem.scrollIntoView({ behavior: ChatMessages._isFirstAutoScroll ? 'instant' : 'smooth' }); + ChatMessages._isFirstAutoScroll = false; + } + }); + } + + _elemIsNearScrollBoundary(elem, threshold) { + const maxScrollPos = document.body.scrollHeight - window.innerHeight; + const remainingScrollDistance = maxScrollPos - window.scrollY; + return remainingScrollDistance < elem.offsetHeight + threshold; + } +}); diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor new file mode 100644 index 0000000000..69ca922a8c --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor @@ -0,0 +1,78 @@ +@inject IChatClient ChatClient + +@if (suggestions is not null) +{ +
+ @foreach (var suggestion in suggestions) + { + + } +
+} + +@code { + private static string Prompt = @" + Suggest up to 3 follow-up questions that I could ask you to help me complete my task. + Each suggestion must be a complete sentence, maximum 6 words. + Each suggestion must be phrased as something that I (the user) would ask you (the assistant) in response to your previous message, + for example 'How do I do that?' or 'Explain ...'. + If there are no suggestions, reply with an empty list. + "; + + private string[]? suggestions; + private CancellationTokenSource? cancellation; + + [Parameter] + public EventCallback OnSelected { get; set; } + + public void Clear() + { + suggestions = null; + cancellation?.Cancel(); + } + + public void Update(IReadOnlyList messages) + { + // Runs in the background and handles its own cancellation/errors + _ = UpdateSuggestionsAsync(messages); + } + + private async Task UpdateSuggestionsAsync(IReadOnlyList messages) + { + cancellation?.Cancel(); + cancellation = new CancellationTokenSource(); + + try + { + var response = await ChatClient.GetResponseAsync( + [.. ReduceMessages(messages), new(ChatRole.User, Prompt)], + cancellationToken: cancellation.Token); + if (!response.TryGetResult(out suggestions)) + { + suggestions = null; + } + + StateHasChanged(); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + await DispatchExceptionAsync(ex); + } + } + + private async Task AddSuggestionAsync(string text) + { + await OnSelected.InvokeAsync(new(ChatRole.User, text)); + } + + private IEnumerable ReduceMessages(IReadOnlyList messages) + { + // Get any leading system messages, plus up to 5 user/assistant messages + // This should be enough context to generate suggestions without unnecessarily resending entire conversations when long + var systemMessages = messages.TakeWhile(m => m.Role == ChatRole.System); + var otherMessages = messages.Where((m, index) => m.Role == ChatRole.User || m.Role == ChatRole.Assistant).Where(m => !string.IsNullOrEmpty(m.Text)).TakeLast(5); + return systemMessages.Concat(otherMessages); + } +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor.css new file mode 100644 index 0000000000..dcc7ee8bd8 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor.css @@ -0,0 +1,9 @@ +.suggestions { + text-align: right; + white-space: nowrap; + gap: 0.5rem; + justify-content: flex-end; + flex-wrap: wrap; + display: flex; + margin-bottom: 0.75rem; +} diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Routes.razor b/dotnet/samples/AGUIWebChat/Client/Components/Routes.razor new file mode 100644 index 0000000000..faa2a8c2d5 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/dotnet/samples/AGUIWebChat/Client/Components/_Imports.razor b/dotnet/samples/AGUIWebChat/Client/Components/_Imports.razor new file mode 100644 index 0000000000..82be3d448e --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Components/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using AGUIWebChatClient +@using AGUIWebChatClient.Components +@using AGUIWebChatClient.Components.Layout +@using Microsoft.Extensions.AI diff --git a/dotnet/samples/AGUIWebChat/Client/Program.cs b/dotnet/samples/AGUIWebChat/Client/Program.cs new file mode 100644 index 0000000000..22a2c11388 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Program.cs @@ -0,0 +1,32 @@ +using AGUIWebChatClient.Components; +using Microsoft.Agents.AI.AGUI; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +string serverUrl = builder.Configuration["SERVER_URL"] ?? "http://localhost:5100"; + +builder.Services.AddHttpClient("aguiserver", httpClient => httpClient.BaseAddress = new Uri(serverUrl)); + +builder.Services.AddChatClient(sp => new AGUIChatClient( + sp.GetRequiredService().CreateClient("aguiserver"), "ag-ui")); + +WebApplication app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseAntiforgery(); +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/dotnet/samples/AGUIWebChat/Client/Properties/launchSettings.json b/dotnet/samples/AGUIWebChat/Client/Properties/launchSettings.json new file mode 100644 index 0000000000..348e16bc3b --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "SERVER_URL": "http://localhost:5100" + } + } + } +} diff --git a/dotnet/samples/AGUIWebChat/Client/wwwroot/app.css b/dotnet/samples/AGUIWebChat/Client/wwwroot/app.css new file mode 100644 index 0000000000..5fd82f3bb0 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Client/wwwroot/app.css @@ -0,0 +1,93 @@ +html { + min-height: 100vh; +} + +html, .main-background-gradient { + background: linear-gradient(to bottom, rgb(225 227 233), #f4f4f4 25rem); +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +html::after { + content: ''; + background-image: linear-gradient(to right, #3a4ed5, #3acfd5 15%, #d53abf 85%, red); + width: 100%; + height: 2px; + position: fixed; + top: 0; +} + +h1 { + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: 600; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.btn-default { + display: flex; + padding: 0.25rem 0.75rem; + gap: 0.25rem; + align-items: center; + border-radius: 0.25rem; + border: 1px solid #9CA3AF; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + background-color: #D1D5DB; +} + + .btn-default:hover { + background-color: #E5E7EB; + } + +.btn-subtle { + display: flex; + padding: 0.25rem 0.75rem; + gap: 0.25rem; + align-items: center; + border-radius: 0.25rem; + border: 1px solid #D1D5DB; + font-size: 0.875rem; + line-height: 1.25rem; +} + + .btn-subtle:hover { + border-color: #93C5FD; + background-color: #DBEAFE; + } + +.page-width { + max-width: 1024px; + margin: auto; +} diff --git a/dotnet/samples/AGUIWebChat/Client/wwwroot/favicon.png b/dotnet/samples/AGUIWebChat/Client/wwwroot/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8422b59695935d180d11d5dbe99653e711097819 GIT binary patch literal 1148 zcmV-?1cUpDP)9h26h2-Cs%i*@Moc3?#6qJID|D#|3|2Hn7gTIYEkr|%Xjp);YgvFmB&0#2E2b=| zkVr)lMv9=KqwN&%obTp-$<51T%rx*NCwceh-E+=&e(oLO`@Z~7gybJ#U|^tB2Pai} zRN@5%1qsZ1e@R(XC8n~)nU1S0QdzEYlWPdUpH{wJ2Pd4V8kI3BM=)sG^IkUXF2-j{ zrPTYA6sxpQ`Q1c6mtar~gG~#;lt=s^6_OccmRd>o{*=>)KS=lM zZ!)iG|8G0-9s3VLm`bsa6e ze*TlRxAjXtm^F8V`M1%s5d@tYS>&+_ga#xKGb|!oUBx3uc@mj1%=MaH4GR0tPBG_& z9OZE;->dO@`Q)nr<%dHAsEZRKl zedN6+3+uGHejJp;Q==pskSAcRcyh@6mjm2z-uG;s%dM-u0*u##7OxI7wwyCGpS?4U zBFAr(%GBv5j$jS@@t@iI8?ZqE36I^4t+P^J9D^ELbS5KMtZ z{Qn#JnSd$15nJ$ggkF%I4yUQC+BjDF^}AtB7w348EL>7#sAsLWs}ndp8^DsAcOIL9 zTOO!!0!k2`9BLk25)NeZp7ev>I1Mn={cWI3Yhx2Q#DnAo4IphoV~R^c0x&nw*MoIV zPthX?{6{u}sMS(MxD*dmd5rU(YazQE59b|TsB5Tm)I4a!VaN@HYOR)DwH1U5y(E)z zQqQU*B%MwtRQ$%x&;1p%ANmc|PkoFJZ%<-uq%PX&C!c-7ypis=eP+FCeuv+B@h#{4 zGx1m0PjS~FJt}3mdt4c!lel`1;4W|03kcZRG+DzkTy|7-F~eDsV2Tx!73dM0H0CTh zl)F-YUkE1zEzEW(;JXc|KR5{ox%YTh{$%F$a36JP6Nb<0%#NbSh$dMYF-{ z1_x(Vx)}fs?5_|!5xBTWiiIQHG<%)*e=45Fhjw_tlnmlixq;mUdC$R8v#j( zhQ$9YR-o%i5Uc`S?6EC51!bTRK=Xkyb<18FkCKnS2;o*qlij1YA@-nRpq#OMTX&RbL<^2q@0qja!uIvI;j$6>~k@IMwD42=8$$!+R^@5o6HX(*n~ httpClient.BaseAddress = new Uri(serverUrl)); + +builder.Services.AddChatClient(sp => new AGUIChatClient( + sp.GetRequiredService().CreateClient("aguiserver"), "ag-ui")); +``` + +The Blazor UI (`Client/Components/Pages/Chat/Chat.razor`) uses the `IChatClient` to: +- Send user messages to the agent +- Stream responses back in real-time +- Maintain conversation history +- Display messages with appropriate styling + +### UI Components + +The chat interface is built from several Blazor components: + +- **Chat.razor** - Main chat page coordinating the conversation flow +- **ChatHeader.razor** - Header with "New chat" button +- **ChatMessageList.razor** - Scrollable list of messages with auto-scroll +- **ChatMessageItem.razor** - Individual message rendering (user vs assistant) +- **ChatInput.razor** - Text input with auto-resize and keyboard shortcuts +- **ChatSuggestions.razor** - AI-generated follow-up question suggestions +- **LoadingSpinner.razor** - Animated loading indicator during streaming + +## Configuration + +### Server Configuration + +The server URL and port are configured in `Server/Properties/launchSettings.json`: + +```json +{ + "profiles": { + "http": { + "applicationUrl": "http://localhost:5100" + } + } +} +``` + +### Client Configuration + +The client connects to the server URL specified in `Client/Properties/launchSettings.json`: + +```json +{ + "profiles": { + "http": { + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "SERVER_URL": "http://localhost:5100" + } + } + } +} +``` + +To change the server URL, modify the `SERVER_URL` environment variable in the client's launch settings or provide it at runtime: + +```powershell +$env:SERVER_URL="http://your-server:5100" +dotnet run +``` + +## Customization + +### Changing the Agent Instructions + +Edit the instructions in `Server/Program.cs`: + +```csharp +ChatClientAgent agent = chatClient.AsIChatClient().CreateAIAgent( + name: "ChatAssistant", + instructions: "You are a helpful coding assistant specializing in C# and .NET."); +``` + +### Styling the UI + +The chat interface uses CSS files colocated with each Razor component. Key styles: + +- `wwwroot/app.css` - Global styles, buttons, color scheme +- `Components/Pages/Chat/Chat.razor.css` - Chat container layout +- `Components/Pages/Chat/ChatMessageItem.razor.css` - Message bubbles and icons +- `Components/Pages/Chat/ChatInput.razor.css` - Input box styling + +### Disabling Suggestions + +To disable the AI-generated follow-up suggestions, comment out the suggestions component in `Chat.razor`: + +```razor +@* *@ +``` diff --git a/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj b/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj new file mode 100644 index 0000000000..f8d134dc23 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj @@ -0,0 +1,23 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/AGUIWebChat/Server/Program.cs b/dotnet/samples/AGUIWebChat/Server/Program.cs new file mode 100644 index 0000000000..8c899353b3 --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Server/Program.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates a basic AG-UI server hosting a chat agent for the Blazor web client. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +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.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 +AzureOpenAIClient azureOpenAIClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()); + +ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName); + +ChatClientAgent agent = chatClient.AsIChatClient().CreateAIAgent( + name: "ChatAssistant", + instructions: "You are a helpful assistant."); + +// Map the AG-UI agent endpoint +app.MapAGUI("/ag-ui", agent); + +await app.RunAsync(); diff --git a/dotnet/samples/AGUIWebChat/Server/Properties/launchSettings.json b/dotnet/samples/AGUIWebChat/Server/Properties/launchSettings.json new file mode 100644 index 0000000000..4d84174f7a --- /dev/null +++ b/dotnet/samples/AGUIWebChat/Server/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} From f04200921ab8c7cfb70d36cdacd7fc3e38b71b10 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 13 Nov 2025 20:09:13 +0100 Subject: [PATCH 2/9] Add AG-UI getting started samples --- dotnet/samples/GettingStarted/AGUI/README.md | 244 ++++++++++++++++ .../Client/Client.csproj | 15 + .../Step01_GettingStarted/Client/Program.cs | 115 ++++++++ .../Step01_GettingStarted/Server/Program.cs | 34 +++ .../Server/Properties/launchSettings.json | 23 ++ .../Server/Server.csproj | 16 ++ .../Server/appsettings.Development.json | 8 + .../Server/appsettings.json | 9 + .../Step01_GettingStarted/test-client.ps1 | 10 + .../Step02_BackendTools/Client/Client.csproj | 15 + .../Step02_BackendTools/Client/Program.cs | 147 ++++++++++ .../Step02_BackendTools/Server/Program.cs | 119 ++++++++ .../Server/Properties/launchSettings.json | 23 ++ .../Step02_BackendTools/Server/Server.csproj | 16 ++ .../Server/appsettings.Development.json | 8 + .../Server/appsettings.json | 9 + .../AGUI/Step02_BackendTools/test-client.ps1 | 10 + .../Step03_FrontendTools/Client/Client.csproj | 15 + .../Step03_FrontendTools/Client/Program.cs | 140 +++++++++ .../Step03_FrontendTools/Server/Program.cs | 34 +++ .../Server/Properties/launchSettings.json | 23 ++ .../Step03_FrontendTools/Server/Server.csproj | 16 ++ .../Server/appsettings.Development.json | 8 + .../Server/appsettings.json | 9 + .../AGUI/Step03_FrontendTools/test-client.ps1 | 10 + .../Step04_HumanInLoop/Client/Client.csproj | 15 + .../AGUI/Step04_HumanInLoop/Client/Program.cs | 154 ++++++++++ .../ServerFunctionApprovalClientAgent.cs | 265 ++++++++++++++++++ .../AGUI/Step04_HumanInLoop/Server/Program.cs | 71 +++++ .../Server/Properties/launchSettings.json | 25 ++ .../Step04_HumanInLoop/Server/Server.csproj | 16 ++ .../ServerFunctionApprovalServerAgent.cs | 261 +++++++++++++++++ .../Server/appsettings.Development.json | 9 + .../Server/appsettings.json | 9 + .../Step04_HumanInLoop.slnx | 4 + .../AGUI/Step04_HumanInLoop/test-client.ps1 | 10 + .../Client/Client.csproj | 15 + .../Step05_StateManagement/Client/Program.cs | 248 ++++++++++++++++ .../Client/StatefulAgent.cs | 88 ++++++ .../Step05_StateManagement/Server/Program.cs | 59 ++++ .../Server/Properties/launchSettings.json | 23 ++ .../Server/RecipeModels.cs | 43 +++ .../Server/Server.csproj | 16 ++ .../Server/SharedStateAgent.cs | 137 +++++++++ .../Server/appsettings.Development.json | 8 + .../Server/appsettings.json | 9 + .../Step05_StateManagement/test-client.ps1 | 10 + 47 files changed, 2571 insertions(+) create mode 100644 dotnet/samples/GettingStarted/AGUI/README.md create mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Properties/launchSettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/appsettings.Development.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/appsettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/test-client.ps1 create mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Properties/launchSettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/appsettings.Development.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/appsettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/test-client.ps1 create mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Properties/launchSettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/appsettings.Development.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/appsettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/test-client.ps1 create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Properties/launchSettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/appsettings.Development.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/appsettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Step04_HumanInLoop.slnx create mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/test-client.ps1 create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Program.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Properties/launchSettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/RecipeModels.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/appsettings.Development.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/appsettings.json create mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/test-client.ps1 diff --git a/dotnet/samples/GettingStarted/AGUI/README.md b/dotnet/samples/GettingStarted/AGUI/README.md new file mode 100644 index 0000000000..a65ecf1a1d --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/README.md @@ -0,0 +1,244 @@ +# AG-UI Getting Started Samples + +This directory contains samples that demonstrate how to build AG-UI (Agent UI Protocol) servers and clients using the Microsoft Agent Framework. + +## Prerequisites + +- .NET 9.0 or later +- Azure OpenAI service endpoint and deployment configured +- Azure CLI installed and authenticated (`az login`) +- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource + +## Environment Variables + +All samples require the following environment variables: + +```bash +export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" +export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +For the client sample, you can optionally set: + +```bash +export AGUI_SERVER_URL="http://localhost:8888" +``` + +## Samples + +### AGUI_Step01_ServerBasic + +A basic AG-UI server that hosts an AI agent accessible via HTTP. Demonstrates: + +- Creating an ASP.NET Core web application +- Setting up an AG-UI server endpoint with `MapAGUI` +- Creating an AI agent from an Azure OpenAI chat client +- Streaming responses via Server-Sent Events (SSE) + +**Run the sample:** + +```bash +cd AGUI_Step01_ServerBasic +dotnet run --urls http://localhost:8888 +``` + +### AGUI_Step02_ClientBasic + +An interactive console client that connects to an AG-UI server. Demonstrates: + +- Creating an AG-UI client with `AGUIAgent` +- Managing conversation threads +- Streaming responses with `RunStreamingAsync` +- Displaying colored console output for different content types +- Using System.CommandLine for interactive input + +**Prerequisites:** AGUI_Step01_ServerBasic (or any AG-UI server) must be running. + +**Run the sample:** + +```bash +cd AGUI_Step02_ClientBasic +dotnet run +``` + +Type messages and press Enter to interact with the agent. Type `:q` or `quit` to exit. + +### AGUI_Step03_ServerWithTools + +An AG-UI server with function tools that execute on the backend. Demonstrates: + +- Creating function tools using `AIFunctionFactory.Create` +- Using `[Description]` attributes for tool documentation +- Defining explicit request/response types for type safety +- Setting up JSON serialization contexts for source generation +- Handling both simple (string) and complex (object) return types + +**Run the sample:** + +```bash +cd AGUI_Step03_ServerWithTools +dotnet run --urls http://localhost:8888 +``` + +This server can be used with the AGUI_Step02_ClientBasic client. Try asking about weather or searching for restaurants. + +### AGUI_Step04_ServerWithState + +An AG-UI server that demonstrates state management with predictive updates. Demonstrates: + +- Defining state schemas using C# records +- Streaming predictive state updates as tools execute +- Managing shared state between client and server +- Using JSON serialization contexts for state types + +**Run the sample:** + +```bash +cd AGUI_Step04_ServerWithState +dotnet run --urls http://localhost:8888 +``` + +### AGUI_Step05_Approvals + +Demonstrates human-in-the-loop approval workflows for sensitive operations. This sample includes both a server and client component. + +#### Server (`AGUI_Step05_Approvals/Server`) + +An AG-UI server that implements approval workflows. Demonstrates: + +- Wrapping tools with `ApprovalRequiredAIFunction` +- Converting `FunctionApprovalRequestContent` to client tool calls +- Middleware pattern for approval request/response handling +- Complete function call capture and restoration + +**Run the server:** + +```bash +cd AGUI_Step05_Approvals/Server +dotnet run --urls http://localhost:8888 +``` + +#### Client (`AGUI_Step05_Approvals/Client`) + +An interactive client that handles approval requests from the server. Demonstrates: + +- Detecting and parsing `"request_approval"` tool calls +- Displaying approval details to users +- Prompting for approval/rejection +- Sending approval responses as tool results +- Resuming conversation after approval + +**Prerequisites:** The approval server must be running. + +**Run the client:** + +```bash +cd AGUI_Step05_Approvals/Client +dotnet run +``` + +Try asking the agent to perform sensitive operations like "Send an email to user@example.com" or "Transfer $500 from account 1234 to account 5678". + +## How AG-UI Works + +### Server-Side + +1. Client sends HTTP POST request with messages +2. ASP.NET Core endpoint receives the request via `MapAGUI` +3. Agent processes messages using Agent Framework +4. Responses are streamed back as Server-Sent Events (SSE) + +### Client-Side + +1. `AGUIAgent` sends HTTP POST request to server +2. Server responds with SSE stream +3. Client parses events into `AgentRunResponseUpdate` objects +4. Updates are displayed based on content type +5. `ConversationId` maintains conversation context + +### Protocol Features + +- **HTTP POST** for requests +- **Server-Sent Events (SSE)** for streaming responses +- **JSON** for event serialization +- **Thread IDs** (as `ConversationId`) for conversation context +- **Run IDs** (as `ResponseId`) for tracking individual executions + +## Troubleshooting + +### Connection Refused + +Ensure the server is running before starting the client: + +```bash +# Terminal 1 +cd AGUI_Step01_ServerBasic +dotnet run --urls http://localhost:8888 + +# Terminal 2 (after server starts) +cd AGUI_Step02_ClientBasic +dotnet run +``` + +### Port Already in Use + +If port 8888 is already in use, choose a different port: + +```bash +# Server +dotnet run --urls http://localhost:8889 + +# Client (set environment variable) +export AGUI_SERVER_URL="http://localhost:8889" +dotnet run +``` + +### Authentication Errors + +Make sure you're authenticated with Azure: + +```bash +az login +``` + +Verify you have the `Cognitive Services OpenAI Contributor` role on the Azure OpenAI resource. + +### Missing Environment Variables + +If you see "AZURE_OPENAI_ENDPOINT is not set" errors, ensure environment variables are set in your current shell session before running the samples. + +### Streaming Not Working + +Check that the client timeout is sufficient (default is 60 seconds). For long-running operations, you may need to increase the timeout in the client code. + +## Next Steps + +After completing these samples, explore more AG-UI capabilities: + +### Currently Available in C# + +The samples above demonstrate the AG-UI features currently available in C#: + +- ✅ **Basic Server and Client**: Setting up AG-UI communication +- ✅ **Backend Tool Rendering**: Function tools that execute on the server +- ✅ **Streaming Responses**: Real-time Server-Sent Events +- ✅ **State Management**: State schemas with predictive updates +- ✅ **Human-in-the-Loop**: Approval workflows for sensitive operations + +### Coming Soon to C# + +The following advanced AG-UI features are available in the Python implementation and are planned for future C# releases: + +- ⏳ **Generative UI**: Custom UI component generation +- ⏳ **Advanced State Patterns**: Complex state synchronization scenarios + +For the most up-to-date AG-UI features, see the [Python samples](../../../../python/samples/) for working examples. + +### Related Documentation + +- [AG-UI Overview](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/) - Complete AG-UI documentation +- [Getting Started Tutorial](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/getting-started) - Step-by-step walkthrough +- [Backend Tool Rendering](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/backend-tool-rendering) - Function tools tutorial +- [Human-in-the-Loop](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/human-in-the-loop) - Approval workflows tutorial +- [State Management](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/state-management) - State management tutorial +- [Agent Framework Overview](https://learn.microsoft.com/azure/agent-framework/overview/agent-framework-overview) - Core framework concepts diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj new file mode 100644 index 0000000000..8c996da81d --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs new file mode 100644 index 0000000000..20154c3d57 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.AGUI; +using Microsoft.Extensions.AI; + +string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888"; + +Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n"); + +// Create the AG-UI client agent +using HttpClient httpClient = new() +{ + Timeout = TimeSpan.FromSeconds(60) +}; + +AGUIChatClient chatClient = new(httpClient, serverUrl); + +AIAgent agent = chatClient.CreateAIAgent( + name: "agui-client", + description: "AG-UI Client Agent"); + +AgentThread thread = agent.GetNewThread(); +List messages = +[ + new(ChatRole.System, "You are a helpful assistant.") +]; + +try +{ + // Check if command-line argument provided + bool autoMode = args.Length > 0; + List queries = autoMode ? new List(args) : new List(); + int queryIndex = 0; + + while (true) + { + string? message; + + if (autoMode) + { + if (queryIndex >= queries.Count) + { + Console.WriteLine("\n[Auto-mode complete]\n"); + break; + } + message = queries[queryIndex++]; + Console.WriteLine($"\nUser: {message}"); + } + else + { + // Get user input + Console.Write("\nUser (:q or quit to exit): "); + message = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(message)) + { + Console.WriteLine("Request cannot be empty."); + continue; + } + + if (message is ":q" or "quit") + { + break; + } + } + + messages.Add(new ChatMessage(ChatRole.User, message)); + + // Stream the response + bool isFirstUpdate = true; + string? threadId = null; + + await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread)) + { + ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); + + // First update indicates run started + if (isFirstUpdate) + { + threadId = chatUpdate.ConversationId; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"\n[Run Started - Thread: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]"); + Console.ResetColor(); + isFirstUpdate = false; + } + + // Display streaming text content + foreach (AIContent content in update.Contents) + { + if (content is TextContent textContent) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write(textContent.Text); + Console.ResetColor(); + } + else if (content is ErrorContent errorContent) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n[Error: {errorContent.Message}]"); + Console.ResetColor(); + } + } + } + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"\n[Run Finished - Thread: {threadId}]"); + Console.ResetColor(); + } +} +catch (Exception ex) +{ + Console.WriteLine($"\nAn error occurred: {ex.Message}"); +} + diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Program.cs new file mode 100644 index 0000000000..f13cdaf8a7 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Program.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +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.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 +ChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName); + +AIAgent agent = chatClient.AsIChatClient().CreateAIAgent( + name: "AGUIAssistant", + instructions: "You are a helpful assistant."); + +// Map the AG-UI agent endpoint +app.MapAGUI("/", agent); + +await app.RunAsync(); diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Properties/launchSettings.json b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Properties/launchSettings.json new file mode 100644 index 0000000000..2bac1b9426 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7047;http://localhost:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj new file mode 100644 index 0000000000..d4fc73a066 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/appsettings.Development.json b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/appsettings.json b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/test-client.ps1 new file mode 100644 index 0000000000..d2abd889e7 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/test-client.ps1 @@ -0,0 +1,10 @@ +$env:AGUI_SERVER_URL="http://localhost:8888" +Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" + +# Test with a simple question +$input = @" +What is 2 + 2? +quit +"@ + +$input | dotnet run diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj new file mode 100644 index 0000000000..8c996da81d --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs new file mode 100644 index 0000000000..b22047bde9 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.AGUI; +using Microsoft.Extensions.AI; + +string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888"; + +Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n"); + +// Create the AG-UI client agent +using HttpClient httpClient = new() +{ + Timeout = TimeSpan.FromSeconds(60) +}; + +AGUIChatClient chatClient = new(httpClient, serverUrl); + +AIAgent agent = chatClient.CreateAIAgent( + name: "agui-client", + description: "AG-UI Client Agent"); + +AgentThread thread = agent.GetNewThread(); +List messages = +[ + new(ChatRole.System, "You are a helpful assistant.") +]; + +try +{ + // Check if command-line argument provided + bool autoMode = args.Length > 0; + List queries = autoMode ? new List(args) : new List(); + int queryIndex = 0; + + while (true) + { + string? message; + + if (autoMode) + { + if (queryIndex >= queries.Count) + { + Console.WriteLine("\n[Auto-mode complete]\n"); + break; + } + message = queries[queryIndex++]; + Console.WriteLine($"\nUser: {message}"); + } + else + { + // Get user input + Console.Write("\nUser (:q or quit to exit): "); + message = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(message)) + { + Console.WriteLine("Request cannot be empty."); + continue; + } + + if (message is ":q" or "quit") + { + break; + } + } + + messages.Add(new ChatMessage(ChatRole.User, message)); + + // Stream the response + bool isFirstUpdate = true; + string? threadId = null; + + await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread)) + { + ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); + + // First update indicates run started + if (isFirstUpdate) + { + threadId = chatUpdate.ConversationId; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"\n[Run Started - Thread: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]"); + Console.ResetColor(); + isFirstUpdate = false; + } + + // Display streaming content + 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}]"); + + // Display individual parameters + if (functionCallContent.Arguments != null) + { + foreach (var kvp in functionCallContent.Arguments) + { + Console.WriteLine($" Parameter: {kvp.Key} = {kvp.Value}"); + } + } + Console.ResetColor(); + break; + + case FunctionResultContent functionResultContent: + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine($"\n[Function Result - CallId: {functionResultContent.CallId}]"); + + if (functionResultContent.Exception != null) + { + Console.WriteLine($" Exception: {functionResultContent.Exception}"); + } + else + { + Console.WriteLine($" Result: {functionResultContent.Result}"); + } + Console.ResetColor(); + break; + + case ErrorContent errorContent: + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n[Error: {errorContent.Message}]"); + Console.ResetColor(); + break; + } + } + } + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"\n[Run Finished - Thread: {threadId}]"); + Console.ResetColor(); + } +} +catch (Exception ex) +{ + Console.WriteLine($"\nAn error occurred: {ex.Message}"); +} + diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Program.cs new file mode 100644 index 0000000000..1f91752b0c --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Program.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using System.Text.Json.Serialization; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Options; +using OpenAI.Chat; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +builder.Services.AddHttpClient().AddLogging(); +builder.Services.ConfigureHttpJsonOptions(options => + options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.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."); + +// Define the function tool +[Description("Search for restaurants in a location.")] +static RestaurantSearchResponse SearchRestaurants( + [Description("The restaurant search request")] RestaurantSearchRequest request) +{ + // Simulated restaurant data + string cuisine = request.Cuisine == "any" ? "Italian" : request.Cuisine; + + return new RestaurantSearchResponse + { + Location = request.Location, + Cuisine = request.Cuisine, + Results = + [ + new RestaurantInfo + { + Name = "The Golden Fork", + Cuisine = cuisine, + Rating = 4.5, + Address = $"123 Main St, {request.Location}" + }, + new RestaurantInfo + { + Name = "Spice Haven", + Cuisine = cuisine == "Italian" ? "Indian" : cuisine, + Rating = 4.7, + Address = $"456 Oak Ave, {request.Location}" + }, + new RestaurantInfo + { + Name = "Green Leaf", + Cuisine = "Vegetarian", + Rating = 4.3, + Address = $"789 Elm Rd, {request.Location}" + } + ] + }; +} + +// Get JsonSerializerOptions from the configured HTTP JSON options +Microsoft.AspNetCore.Http.Json.JsonOptions jsonOptions = app.Services.GetRequiredService>().Value; + +// Create tool with serializer options +AITool[] tools = +[ + AIFunctionFactory.Create( + SearchRestaurants, + serializerOptions: jsonOptions.SerializerOptions) +]; + +// Create the AI agent with tools +ChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName); + +ChatClientAgent agent = chatClient.AsIChatClient().CreateAIAgent( + name: "AGUIAssistant", + instructions: "You are a helpful assistant with access to restaurant information.", + tools: tools); + +// Map the AG-UI agent endpoint +app.MapAGUI("/", agent); + +await app.RunAsync(); + +// Define request/response types for the tool +internal sealed class RestaurantSearchRequest +{ + public string Location { get; set; } = string.Empty; + public string Cuisine { get; set; } = "any"; +} + +internal sealed class RestaurantSearchResponse +{ + public string Location { get; set; } = string.Empty; + public string Cuisine { get; set; } = string.Empty; + public RestaurantInfo[] Results { get; set; } = []; +} + +internal sealed class RestaurantInfo +{ + public string Name { get; set; } = string.Empty; + public string Cuisine { get; set; } = string.Empty; + public double Rating { get; set; } + public string Address { get; set; } = string.Empty; +} + +// JSON serialization context for source generation +[JsonSerializable(typeof(RestaurantSearchRequest))] +[JsonSerializable(typeof(RestaurantSearchResponse))] +internal sealed partial class SampleJsonSerializerContext : JsonSerializerContext; + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Properties/launchSettings.json b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Properties/launchSettings.json new file mode 100644 index 0000000000..2bac1b9426 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7047;http://localhost:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj new file mode 100644 index 0000000000..d4fc73a066 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/appsettings.Development.json b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/appsettings.json b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/test-client.ps1 new file mode 100644 index 0000000000..d2abd889e7 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/test-client.ps1 @@ -0,0 +1,10 @@ +$env:AGUI_SERVER_URL="http://localhost:8888" +Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" + +# Test with a simple question +$input = @" +What is 2 + 2? +quit +"@ + +$input | dotnet run diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj new file mode 100644 index 0000000000..8c996da81d --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs new file mode 100644 index 0000000000..2a418bd523 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.AGUI; +using Microsoft.Extensions.AI; + +string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888"; + +Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n"); + +// Define a frontend function tool +[Description("Get the user's current location from GPS.")] +static string GetUserLocation() +{ + // Access client-side GPS + return "Amsterdam, Netherlands (52.37°N, 4.90°E)"; +} + +// Create frontend tools +AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation)]; + +// Create the AG-UI client agent with tools +using HttpClient httpClient = new() +{ + Timeout = TimeSpan.FromSeconds(60) +}; + +AGUIChatClient chatClient = new(httpClient, serverUrl); + +AIAgent agent = chatClient.CreateAIAgent( + name: "agui-client", + description: "AG-UI Client Agent", + tools: frontendTools); + +AgentThread thread = agent.GetNewThread(); +List messages = +[ + new(ChatRole.System, "You are a helpful assistant.") +]; + +try +{ + // Check if command-line argument provided + bool autoMode = args.Length > 0; + List queries = autoMode ? new List(args) : new List(); + int queryIndex = 0; + + while (true) + { + string? message; + + if (autoMode) + { + if (queryIndex >= queries.Count) + { + Console.WriteLine("\n[Auto-mode complete]\n"); + break; + } + message = queries[queryIndex++]; + Console.WriteLine($"\nUser: {message}"); + } + else + { + // Get user input + Console.Write("\nUser (:q or quit to exit): "); + message = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(message)) + { + Console.WriteLine("Request cannot be empty."); + continue; + } + + if (message is ":q" or "quit") + { + break; + } + } + + messages.Add(new ChatMessage(ChatRole.User, message)); + + // Stream the response + bool isFirstUpdate = true; + string? threadId = null; + + await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread)) + { + ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); + + // First update indicates run started + if (isFirstUpdate) + { + threadId = chatUpdate.ConversationId; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"\n[Run Started - Thread: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]"); + Console.ResetColor(); + isFirstUpdate = false; + } + + // Display streaming content + foreach (AIContent content in update.Contents) + { + if (content is TextContent textContent) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write(textContent.Text); + Console.ResetColor(); + } + else if (content is FunctionCallContent functionCallContent) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"\n[Client Tool Call - Name: {functionCallContent.Name}]"); + Console.ResetColor(); + } + else if (content is FunctionResultContent functionResultContent) + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine($"[Client Tool Result: {functionResultContent.Result}]"); + Console.ResetColor(); + } + else if (content is ErrorContent errorContent) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n[Error: {errorContent.Message}]"); + Console.ResetColor(); + } + } + } + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"\n[Run Finished - Thread: {threadId}]"); + Console.ResetColor(); + } +} +catch (Exception ex) +{ + Console.WriteLine($"\nAn error occurred: {ex.Message}"); +} + diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Program.cs new file mode 100644 index 0000000000..f13cdaf8a7 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Program.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +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.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 +ChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName); + +AIAgent agent = chatClient.AsIChatClient().CreateAIAgent( + name: "AGUIAssistant", + instructions: "You are a helpful assistant."); + +// Map the AG-UI agent endpoint +app.MapAGUI("/", agent); + +await app.RunAsync(); diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Properties/launchSettings.json b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Properties/launchSettings.json new file mode 100644 index 0000000000..2bac1b9426 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7047;http://localhost:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj new file mode 100644 index 0000000000..d4fc73a066 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/appsettings.Development.json b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/appsettings.json b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/test-client.ps1 new file mode 100644 index 0000000000..d2abd889e7 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/test-client.ps1 @@ -0,0 +1,10 @@ +$env:AGUI_SERVER_URL="http://localhost:8888" +Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" + +# Test with a simple question +$input = @" +What is 2 + 2? +quit +"@ + +$input | dotnet run diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj new file mode 100644 index 0000000000..30fe7c668f --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Program.cs new file mode 100644 index 0000000000..4d01e7d2d1 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Program.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.AGUI; +using Microsoft.Extensions.AI; + +string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:5100"; + +// Connect to the AG-UI server +using HttpClient httpClient = new() +{ + Timeout = TimeSpan.FromSeconds(60) +}; + +AGUIChatClient chatClient = new(httpClient, serverUrl); + +// Create agent +ChatClientAgent baseAgent = chatClient.CreateAIAgent( + name: "AGUIAssistant", + instructions: "You are a helpful assistant."); + +// Use default JSON serializer options +JsonSerializerOptions jsonSerializerOptions = JsonSerializerOptions.Default; + +// Wrap the agent with ServerFunctionApprovalClientAgent +ServerFunctionApprovalClientAgent agent = new(baseAgent, jsonSerializerOptions); + +List messages = []; +AgentThread? thread = null; + +Console.ForegroundColor = ConsoleColor.White; +Console.WriteLine("Ask a question (or type 'exit' to quit):"); +Console.ResetColor(); + +string? input; +while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringComparison.OrdinalIgnoreCase)) +{ + if (string.IsNullOrWhiteSpace(input)) + { + continue; + } + + messages.Add(new ChatMessage(ChatRole.User, input)); + Console.WriteLine(); + +#pragma warning disable MEAI001 + List approvalResponses = []; + + do + { + approvalResponses.Clear(); + + List chatResponseUpdates = []; + await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: default)) + { + chatResponseUpdates.Add(update); + foreach (AIContent content in update.Contents) + { + switch (content) + { + case FunctionApprovalRequestContent approvalRequest: + DisplayApprovalRequest(approvalRequest); + + Console.Write($"\nApprove '{approvalRequest.FunctionCall.Name}'? (yes/no): "); + string? userInput = Console.ReadLine(); + bool approved = userInput?.ToUpperInvariant() is "YES" or "Y"; + + FunctionApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved); + + if (approvalRequest.AdditionalProperties != null) + { + approvalResponse.AdditionalProperties = new AdditionalPropertiesDictionary(); + foreach (var kvp in approvalRequest.AdditionalProperties) + { + approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value; + } + } + + approvalResponses.Add(approvalResponse); + break; + + case TextContent textContent: + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write(textContent.Text); + Console.ResetColor(); + break; + + case FunctionCallContent functionCall: + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"[Tool Call - Name: {functionCall.Name}]"); + if (functionCall.Arguments is { } arguments) + { + Console.WriteLine($" Parameters: {JsonSerializer.Serialize(arguments)}"); + } + Console.ResetColor(); + break; + + case FunctionResultContent functionResult: + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine($"[Tool Result: {functionResult.Result}]"); + Console.ResetColor(); + break; + + case ErrorContent error: + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[Error: {error.Message}]"); + Console.ResetColor(); + break; + } + } + } + + AgentRunResponse response = chatResponseUpdates.ToAgentRunResponse(); + messages.AddRange(response.Messages); + foreach (AIContent approvalResponse in approvalResponses) + { + messages.Add(new ChatMessage(ChatRole.Tool, [approvalResponse])); + } + } + while (approvalResponses.Count > 0); +#pragma warning restore MEAI001 + + Console.WriteLine("\n"); + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine("Ask another question (or type 'exit' to quit):"); + Console.ResetColor(); +} + +#pragma warning disable MEAI001 +static void DisplayApprovalRequest(FunctionApprovalRequestContent approvalRequest) +{ + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(); + Console.WriteLine("============================================================"); + Console.WriteLine("APPROVAL REQUIRED"); + Console.WriteLine("============================================================"); + Console.WriteLine($"Function: {approvalRequest.FunctionCall.Name}"); + + if (approvalRequest.FunctionCall.Arguments != null) + { + Console.WriteLine("Arguments:"); + foreach (var arg in approvalRequest.FunctionCall.Arguments) + { + Console.WriteLine($" {arg.Key} = {arg.Value}"); + } + } + + Console.WriteLine("============================================================"); + Console.ResetColor(); +} +#pragma warning restore MEAI001 diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs new file mode 100644 index 0000000000..c94c5ea79c --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +/// +/// A delegating agent that handles server function approval requests and responses. +/// Transforms between FunctionApprovalRequestContent/FunctionApprovalResponseContent +/// and the server's request_approval tool call pattern. +/// +internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent +{ + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public ServerFunctionApprovalClientAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions) + : base(innerAgent) + { + _jsonSerializerOptions = jsonSerializerOptions; + } + + public override Task RunAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + return RunStreamingAsync(messages, thread, options, cancellationToken) + .ToAgentRunResponseAsync(cancellationToken); + } + + public override async IAsyncEnumerable RunStreamingAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Process and transform approval messages, creating a new message list + var processedMessages = ProcessOutgoingServerFunctionApprovals(messages, _jsonSerializerOptions); + + // Run the inner agent and intercept any approval requests + await foreach (var update in InnerAgent.RunStreamingAsync( + processedMessages, thread, options, cancellationToken).ConfigureAwait(false)) + { + yield return ProcessIncomingServerApprovalRequests(update, _jsonSerializerOptions); + } + } + +#pragma warning disable MEAI001 // Type is for evaluation purposes only + private static FunctionResultContent ConvertApprovalResponseToToolResult(FunctionApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions) + { + return new FunctionResultContent( + callId: approvalResponse.Id, + result: JsonSerializer.SerializeToElement( + new ApprovalResponse + { + ApprovalId = approvalResponse.Id, + Approved = approvalResponse.Approved + }, + jsonOptions)); + } + + private static List CopyMessagesUpToIndex(IList messages, int index) + { + var result = new List(index); + for (int i = 0; i < index; i++) + { + result.Add(messages[i]); + } + return result; + } + + private static List CopyContentsUpToIndex(IList contents, int index) + { + var result = new List(index); + for (int i = 0; i < index; i++) + { + result.Add(contents[i]); + } + return result; + } + + private static IEnumerable ProcessOutgoingServerFunctionApprovals( + IEnumerable messages, + JsonSerializerOptions jsonSerializerOptions) + { + var messagesList = messages.ToList(); + List? result = null; + + Dictionary approvalRequests = []; + for (var messageIndex = 0; messageIndex < messagesList.Count; messageIndex++) + { + var message = messagesList[messageIndex]; + List? transformedContents = null; + + // Process each content item in the message + HashSet approvalCalls = []; + for (var contentIndex = 0; contentIndex < message.Contents.Count; contentIndex++) + { + var content = message.Contents[contentIndex]; + + // Handle pending approval requests (transform to tool call) + if (content is FunctionApprovalRequestContent approvalRequest && + approvalRequest.AdditionalProperties?.TryGetValue("original_function", out var originalFunction) == true && + originalFunction is FunctionCallContent original) + { + approvalRequests[approvalRequest.Id] = approvalRequest; + transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex); + transformedContents.Add(original); + } + // Handle pending approval responses (transform to tool result) + else if (content is FunctionApprovalResponseContent approvalResponse && + approvalRequests.TryGetValue(approvalResponse.Id, out var correspondingRequest)) + { + transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex); + transformedContents.Add(ConvertApprovalResponseToToolResult(approvalResponse, jsonSerializerOptions)); + approvalRequests.Remove(approvalResponse.Id); + correspondingRequest.AdditionalProperties?.Remove("original_function"); + } + // Skip historical approval content + else if (content is FunctionCallContent { Name: "request_approval" } approvalCall) + { + transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex); + approvalCalls.Add(approvalCall.CallId); + } + else if (content is FunctionResultContent functionResult && + approvalCalls.Contains(functionResult.CallId)) + { + transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex); + approvalCalls.Remove(functionResult.CallId); + } + else if (transformedContents != null) + { + transformedContents.Add(content); + } + } + + if (transformedContents != null && transformedContents.Count == 0) + { + transformedContents = null; // Entire message is skipped + continue; + } + else if (transformedContents != null) + { + // We made changes to contents, so use transformedContents + var newMessage = new ChatMessage(message.Role, transformedContents) + { + AuthorName = message.AuthorName, + MessageId = message.MessageId, + CreatedAt = message.CreatedAt, + RawRepresentation = message.RawRepresentation, + AdditionalProperties = message.AdditionalProperties + }; + result ??= CopyMessagesUpToIndex(messagesList, messageIndex); + result.Add(newMessage); + } + else if (result != null) + { + // We're already copying messages, so copy this unchanged message too + result.Add(message); + } + // If result is null, we haven't made any changes yet, so keep processing + } + + return result ?? messagesList; + } + + private static AgentRunResponseUpdate ProcessIncomingServerApprovalRequests( + AgentRunResponseUpdate update, + JsonSerializerOptions jsonSerializerOptions) + { + IList? updatedContents = null; + for (var i = 0; i < update.Contents.Count; i++) + { + var content = update.Contents[i]; + if (content is FunctionCallContent { Name: "request_approval" } request) + { + updatedContents ??= [.. update.Contents]; + + // Serialize the function arguments as JsonElement + ApprovalRequest? approvalRequest; + if (request.Arguments?.TryGetValue("request", out var reqObj) == true && + reqObj is JsonElement je) + { + approvalRequest = (ApprovalRequest?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))); + } + else + { + approvalRequest = null; + } + + if (approvalRequest == null) + { + throw new InvalidOperationException("Failed to deserialize approval request."); + } + + var functionCallArgs = (Dictionary?)approvalRequest.FunctionArguments? + .Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(Dictionary))); + + var aprovalRequestContent = new FunctionApprovalRequestContent( + id: approvalRequest.ApprovalId, + new FunctionCallContent( + callId: approvalRequest.ApprovalId, + name: approvalRequest.FunctionName, + arguments: functionCallArgs)); + + aprovalRequestContent.AdditionalProperties ??= []; + aprovalRequestContent.AdditionalProperties["original_function"] = content; + + updatedContents[i] = aprovalRequestContent; + } + } + + if (updatedContents is not null) + { + var chatUpdate = update.AsChatResponseUpdate(); + return new AgentRunResponseUpdate(new ChatResponseUpdate() + { + Role = chatUpdate.Role, + Contents = updatedContents, + MessageId = chatUpdate.MessageId, + AuthorName = chatUpdate.AuthorName, + CreatedAt = chatUpdate.CreatedAt, + RawRepresentation = chatUpdate.RawRepresentation, + ResponseId = chatUpdate.ResponseId, + AdditionalProperties = chatUpdate.AdditionalProperties + }) + { + AgentId = update.AgentId, + ContinuationToken = update.ContinuationToken, + }; + } + else + { + return update; + } + } +} +#pragma warning restore MEAI001 + +public sealed class ApprovalRequest +{ + [JsonPropertyName("approval_id")] + public required string ApprovalId { get; init; } + + [JsonPropertyName("function_name")] + public required string FunctionName { get; init; } + + [JsonPropertyName("function_arguments")] + public JsonElement? FunctionArguments { get; init; } + + [JsonPropertyName("message")] + public string? Message { get; init; } +} + +public sealed class ApprovalResponse +{ + [JsonPropertyName("approval_id")] + public required string ApprovalId { get; init; } + + [JsonPropertyName("approved")] + public required bool Approved { get; init; } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs new file mode 100644 index 0000000000..5183f88b13 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.HttpLogging; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Options; +using OpenAI.Chat; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +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(ApprovalJsonContext.Default)); +builder.Services.AddAGUI(); + +WebApplication app = builder.Build(); + +app.UseHttpLogging(); + +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."); + +// Define approval-required tool +[Description("Approve the expense report.")] +static string ApproveExpenseReport(string expenseReportId) +{ + return $"Expense report {expenseReportId} approved"; +} + +// Get JsonSerializerOptions +var jsonOptions = app.Services.GetRequiredService>().Value; + +// Create approval-required tool +#pragma warning disable MEAI001 // Type is for evaluation purposes only +AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(ApproveExpenseReport))]; +#pragma warning restore MEAI001 + +// Create base agent +ChatClient openAIChatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName); + +ChatClientAgent baseAgent = openAIChatClient.AsIChatClient().CreateAIAgent( + name: "AGUIAssistant", + instructions: "You are a helpful assistant in charge of approving expenses", + tools: tools); + +// Wrap with ServerFunctionApprovalAgent +var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions); + +app.MapAGUI("/", agent); +await app.RunAsync(); diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Properties/launchSettings.json b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Properties/launchSettings.json new file mode 100644 index 0000000000..3eb863e0cd --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7047;http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "AZURE_OPENAI_ENDPOINT": "https://ag-ui-agent-framework.openai.azure.com/", + "AZURE_OPENAI_DEPLOYMENT_NAME": "gpt-4.1-mini" + } + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj new file mode 100644 index 0000000000..a25b01e59d --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + enable + enable + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs new file mode 100644 index 0000000000..d4f50a7373 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +/// +/// A delegating agent that handles function approval requests on the server side. +/// Transforms between FunctionApprovalRequestContent/FunctionApprovalResponseContent +/// and the request_approval tool call pattern for client communication. +/// +internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent +{ + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public ServerFunctionApprovalAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions) + : base(innerAgent) + { + _jsonSerializerOptions = jsonSerializerOptions; + } + + public override Task RunAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + return RunStreamingAsync(messages, thread, options, cancellationToken) + .ToAgentRunResponseAsync(cancellationToken); + } + + public override async IAsyncEnumerable RunStreamingAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Process and transform incoming approval responses from client, creating a new message list + var processedMessages = ProcessIncomingFunctionApprovals(messages, _jsonSerializerOptions); + + // Run the inner agent and intercept any approval requests + await foreach (var update in InnerAgent.RunStreamingAsync( + processedMessages, thread, options, cancellationToken).ConfigureAwait(false)) + { + yield return ProcessOutgoingApprovalRequests(update, _jsonSerializerOptions); + } + } + +#pragma warning disable MEAI001 // Type is for evaluation purposes only + private static FunctionApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions) + { + if (toolCall.Name != "request_approval" || toolCall.Arguments == null) + { + throw new InvalidOperationException("Invalid request_approval tool call"); + } + + var request = toolCall.Arguments.TryGetValue("request", out var reqObj) && + reqObj is JsonElement argsElement && + argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest && + approvalRequest != null ? approvalRequest : null; + + if (request == null) + { + throw new InvalidOperationException("Failed to deserialize approval request from tool call"); + } + + return new FunctionApprovalRequestContent( + id: request.ApprovalId, + new FunctionCallContent( + callId: request.ApprovalId, + name: request.FunctionName, + arguments: request.FunctionArguments)); + } + + private static FunctionApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, FunctionApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions) + { + var approvalResponse = result.Result is JsonElement je ? + (ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) : + result.Result is string str ? + (ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) : + result.Result as ApprovalResponse; + + if (approvalResponse == null) + { + throw new InvalidOperationException("Failed to deserialize approval response from tool result"); + } + + return approval.CreateResponse(approvalResponse.Approved); + } +#pragma warning restore MEAI001 + + private static List CopyMessagesUpToIndex(IList messages, int index) + { + var result = new List(index); + for (int i = 0; i < index; i++) + { + result.Add(messages[i]); + } + return result; + } + + private static List CopyContentsUpToIndex(IList contents, int index) + { + var result = new List(index); + for (int i = 0; i < index; i++) + { + result.Add(contents[i]); + } + return result; + } + + private static IList ProcessIncomingFunctionApprovals( + IEnumerable messages, + JsonSerializerOptions jsonSerializerOptions) + { + var messagesList = messages.ToList(); + List? result = null; + + // Track approval ID to original call ID mapping + var approvalIdToCallId = new Dictionary(); +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + Dictionary trackedRequestApprovalToolCalls = new(); // Remote approvals + for (int messageIndex = 0; messageIndex < messagesList.Count; messageIndex++) + { + var message = messagesList[messageIndex]; + List? transformedContents = null; + for (int j = 0; j < message.Contents.Count; j++) + { + var content = message.Contents[j]; + if (content is FunctionCallContent { Name: "request_approval" } toolCall) + { + result ??= CopyMessagesUpToIndex(messagesList, messageIndex); + transformedContents ??= CopyContentsUpToIndex(message.Contents, j); + var approvalRequest = ConvertToolCallToApprovalRequest(toolCall, jsonSerializerOptions); + transformedContents.Add(approvalRequest); + trackedRequestApprovalToolCalls[toolCall.CallId] = approvalRequest; + result.Add(new ChatMessage(message.Role, transformedContents) + { + AuthorName = message.AuthorName, + MessageId = message.MessageId, + CreatedAt = message.CreatedAt, + RawRepresentation = message.RawRepresentation, + AdditionalProperties = message.AdditionalProperties + }); + } + else if (content is FunctionResultContent toolResult && + trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval) == true) + { + result ??= CopyMessagesUpToIndex(messagesList, messageIndex); + transformedContents ??= CopyContentsUpToIndex(message.Contents, j); + var approvalResponse = ConvertToolResultToApprovalResponse(toolResult, approval, jsonSerializerOptions); + transformedContents.Add(approvalResponse); + result.Add(new ChatMessage(message.Role, transformedContents) + { + AuthorName = message.AuthorName, + MessageId = message.MessageId, + CreatedAt = message.CreatedAt, + RawRepresentation = message.RawRepresentation, + AdditionalProperties = message.AdditionalProperties + }); + } + else if (result != null) + { + result.Add(message); + } + } + } +#pragma warning restore MEAI001 + + return result ?? messagesList; + } + + private static AgentRunResponseUpdate ProcessOutgoingApprovalRequests( + AgentRunResponseUpdate update, + JsonSerializerOptions jsonSerializerOptions) + { + IList? updatedContents = null; + for (var i = 0; i < update.Contents.Count; i++) + { + var content = update.Contents[i]; +#pragma warning disable MEAI001 // Type is for evaluation purposes only + if (content is FunctionApprovalRequestContent request) + { + updatedContents ??= [.. update.Contents]; + var functionCall = request.FunctionCall; + var approvalId = request.Id; + + var approvalData = new ApprovalRequest + { + ApprovalId = approvalId, + FunctionName = functionCall.Name, + FunctionArguments = functionCall.Arguments, + Message = $"Approve execution of '{functionCall.Name}'?" + }; + + updatedContents[i] = new FunctionCallContent( + callId: approvalId, + name: "request_approval", + arguments: new Dictionary { ["request"] = approvalData }); + } +#pragma warning restore MEAI001 + } + + if (updatedContents is not null) + { + var chatUpdate = update.AsChatResponseUpdate(); + // Yield a tool call update that represents the approval request + return new AgentRunResponseUpdate(new ChatResponseUpdate() + { + Role = chatUpdate.Role, + Contents = updatedContents, + MessageId = chatUpdate.MessageId, + AuthorName = chatUpdate.AuthorName, + CreatedAt = chatUpdate.CreatedAt, + RawRepresentation = chatUpdate.RawRepresentation, + ResponseId = chatUpdate.ResponseId, + AdditionalProperties = chatUpdate.AdditionalProperties + }) + { + AgentId = update.AgentId, + ContinuationToken = update.ContinuationToken + }; + } + else + { + return update; + } + } +} + +// Define approval models +public sealed class ApprovalRequest +{ + [JsonPropertyName("approval_id")] + public required string ApprovalId { get; init; } + + [JsonPropertyName("function_name")] + public required string FunctionName { get; init; } + + [JsonPropertyName("function_arguments")] + public IDictionary? FunctionArguments { get; init; } + + [JsonPropertyName("message")] + public string? Message { get; init; } +} + +public sealed class ApprovalResponse +{ + [JsonPropertyName("approval_id")] + public required string ApprovalId { get; init; } + + [JsonPropertyName("approved")] + public required bool Approved { get; init; } +} + +[JsonSerializable(typeof(ApprovalRequest))] +[JsonSerializable(typeof(ApprovalResponse))] +[JsonSerializable(typeof(Dictionary))] +public sealed partial class ApprovalJsonContext : JsonSerializerContext; diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/appsettings.Development.json b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/appsettings.Development.json new file mode 100644 index 0000000000..3e805edef8 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information" + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/appsettings.json b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Step04_HumanInLoop.slnx b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Step04_HumanInLoop.slnx new file mode 100644 index 0000000000..969c14884b --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Step04_HumanInLoop.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/test-client.ps1 new file mode 100644 index 0000000000..d2abd889e7 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/test-client.ps1 @@ -0,0 +1,10 @@ +$env:AGUI_SERVER_URL="http://localhost:8888" +Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" + +# Test with a simple question +$input = @" +What is 2 + 2? +quit +"@ + +$input | dotnet run diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj new file mode 100644 index 0000000000..8c996da81d --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs new file mode 100644 index 0000000000..3235c5199d --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.AGUI; +using Microsoft.Extensions.AI; +using RecipeClient; + +// Check for command-line arguments for automated testing +string[] queries = args.Length > 0 ? args : []; +int queryIndex = 0; + +string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888"; + +Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n"); + +// Create the AG-UI client agent +using HttpClient httpClient = new() +{ + Timeout = TimeSpan.FromSeconds(60) +}; + +AGUIChatClient chatClient = new(httpClient, serverUrl); + +AIAgent baseAgent = chatClient.CreateAIAgent( + name: "recipe-client", + description: "AG-UI Recipe Client Agent"); + +// Wrap the base agent with state management +JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web) +{ + TypeInfoResolver = RecipeSerializerContext.Default +}; +StatefulAgent agent = new(baseAgent, jsonOptions, new AgentState()); + +AgentThread thread = agent.GetNewThread(); +List messages = +[ + new(ChatRole.System, "You are a helpful recipe assistant.") +]; + +try +{ + while (true) + { + string? message; + + if (queries.Length > 0) + { + if (queryIndex >= queries.Length) + { + Console.WriteLine("\n[Auto-mode complete]\n"); + break; + } + message = queries[queryIndex++]; + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine($"\nQuery: {message}"); + Console.ResetColor(); + } + else + { + // Get user input + Console.Write("\nUser (:q to quit, :state to show state): "); + message = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(message)) + { + Console.WriteLine("Request cannot be empty."); + continue; + } + + if (message is ":q" or "quit") + { + break; + } + + if (message.Equals(":state", StringComparison.OrdinalIgnoreCase)) + { + DisplayState(agent.State.Recipe); + continue; + } + } + + messages.Add(new ChatMessage(ChatRole.User, message)); + + // Stream the response + bool isFirstUpdate = true; + string? threadId = null; + bool stateReceived = false; + + Console.WriteLine(); + + await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread)) + { + ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); + + // First update indicates run started + if (isFirstUpdate) + { + threadId = chatUpdate.ConversationId; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[Run Started - Thread: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]"); + Console.ResetColor(); + isFirstUpdate = false; + } + + // Display streaming content + foreach (AIContent content in update.Contents) + { + switch (content) + { + case TextContent textContent: + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write(textContent.Text); + Console.ResetColor(); + break; + + case DataContent dataContent when dataContent.MediaType == "application/json": + // This is a state snapshot - the StatefulAgent has already updated the state + stateReceived = true; + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("\n[State Snapshot Received]"); + Console.ResetColor(); + break; + + case ErrorContent errorContent: + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n[Error: {errorContent.Message}]"); + Console.ResetColor(); + break; + } + } + } + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"\n[Run Finished - Thread: {threadId}]"); + Console.ResetColor(); + + // Display final state if received + if (stateReceived) + { + DisplayState(agent.State.Recipe); + } + + // Exit after one query in automated mode + if (queries.Length > 0 && queryIndex >= queries.Length) + { + break; + } + } +} +catch (Exception ex) +{ + Console.WriteLine($"\nAn error occurred: {ex.Message}"); +} + +static void DisplayState(RecipeState? state) +{ + if (state == null) + { + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine("\n[No state available]"); + Console.ResetColor(); + return; + } + + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("\n" + new string('=', 60)); + Console.WriteLine("CURRENT STATE"); + Console.WriteLine(new string('=', 60)); + Console.ResetColor(); + + if (!string.IsNullOrEmpty(state.Title)) + { + Console.WriteLine($"\nRecipe:"); + Console.WriteLine($" Title: {state.Title}"); + if (!string.IsNullOrEmpty(state.Cuisine)) + Console.WriteLine($" Cuisine: {state.Cuisine}"); + if (!string.IsNullOrEmpty(state.SkillLevel)) + Console.WriteLine($" Skill Level: {state.SkillLevel}"); + if (state.PrepTimeMinutes > 0) + Console.WriteLine($" Prep Time: {state.PrepTimeMinutes} minutes"); + if (state.CookTimeMinutes > 0) + Console.WriteLine($" Cook Time: {state.CookTimeMinutes} minutes"); + + if (state.Ingredients.Count > 0) + { + Console.WriteLine($"\n Ingredients:"); + foreach (var ingredient in state.Ingredients) + { + Console.WriteLine($" - {ingredient}"); + } + } + + if (state.Steps.Count > 0) + { + Console.WriteLine($"\n Steps:"); + for (int i = 0; i < state.Steps.Count; i++) + { + Console.WriteLine($" {i + 1}. {state.Steps[i]}"); + } + } + } + + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("\n" + new string('=', 60)); + Console.ResetColor(); +} + +// State wrapper +internal sealed class AgentState +{ + [JsonPropertyName("recipe")] + public RecipeState Recipe { get; set; } = new(); +} + +// Recipe state model +internal sealed class RecipeState +{ + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("cuisine")] + public string Cuisine { get; set; } = string.Empty; + + [JsonPropertyName("ingredients")] + public List Ingredients { get; set; } = []; + + [JsonPropertyName("steps")] + public List Steps { get; set; } = []; + + [JsonPropertyName("prep_time_minutes")] + public int PrepTimeMinutes { get; set; } + + [JsonPropertyName("cook_time_minutes")] + public int CookTimeMinutes { get; set; } + + [JsonPropertyName("skill_level")] + public string SkillLevel { get; set; } = string.Empty; +} + +// JSON serialization context +[JsonSerializable(typeof(AgentState))] +[JsonSerializable(typeof(RecipeState))] +[JsonSerializable(typeof(JsonElement))] +internal sealed partial class RecipeSerializerContext : JsonSerializerContext; + diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs new file mode 100644 index 0000000000..8e93b9ac25 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using System.Text.Json; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +namespace RecipeClient; + +/// +/// A delegating agent that manages client-side state and automatically attaches it to requests. +/// +/// The state type. +internal sealed class StatefulAgent : DelegatingAIAgent + where TState : class, new() +{ + private readonly JsonSerializerOptions _jsonSerializerOptions; + + /// + /// Gets or sets the current state. + /// + public TState State { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The underlying agent to delegate to. + /// The JSON serializer options for state serialization. + /// The initial state. If null, a new instance will be created. + public StatefulAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions, TState? initialState = null) + : base(innerAgent) + { + this._jsonSerializerOptions = jsonSerializerOptions; + this.State = initialState ?? new TState(); + } + + /// + public override Task RunAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + return this.RunStreamingAsync(messages, thread, options, cancellationToken) + .ToAgentRunResponseAsync(cancellationToken); + } + + /// + public override async IAsyncEnumerable RunStreamingAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Add state to messages + List messagesWithState = [.. messages]; + + // Serialize the state using AgentState wrapper + byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes( + this.State, + this._jsonSerializerOptions.GetTypeInfo(typeof(TState))); + DataContent stateContent = new(stateBytes, "application/json"); + ChatMessage stateMessage = new(ChatRole.System, [stateContent]); + messagesWithState.Add(stateMessage); + + // Stream the response and update state when received + await foreach (AgentRunResponseUpdate update in this.InnerAgent.RunStreamingAsync(messagesWithState, thread, options, cancellationToken)) + { + // Check if this update contains a state snapshot + foreach (AIContent content in update.Contents) + { + if (content is DataContent dataContent && dataContent.MediaType == "application/json") + { + // Deserialize the state + TState? newState = JsonSerializer.Deserialize( + dataContent.Data.Span, + this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) as TState; + if (newState != null) + { + this.State = newState; + } + } + } + + yield return update; + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Program.cs new file mode 100644 index 0000000000..9139fef598 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Program.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Options; +using OpenAI.Chat; +using RecipeAssistant; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +builder.Services.AddHttpClient().AddLogging(); +builder.Services.ConfigureHttpJsonOptions(options => + options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default)); +builder.Services.AddAGUI(); + +// Configure to listen on port 8888 +builder.WebHost.UseUrls("http://localhost:8888"); + +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."); + +// Get JsonSerializerOptions +var jsonOptions = app.Services.GetRequiredService>().Value; + +// Create base agent +ChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName); + +AIAgent baseAgent = chatClient.AsIChatClient().CreateAIAgent( + name: "RecipeAgent", + instructions: """ + You are a helpful recipe assistant. When users ask you to create or suggest a recipe, + respond with a complete AgentState JSON object that includes: + - recipe.title: The recipe name + - recipe.cuisine: Type of cuisine (e.g., Italian, Mexican, Japanese) + - recipe.ingredients: Array of ingredient strings with quantities + - recipe.steps: Array of cooking instruction strings + - recipe.prep_time_minutes: Preparation time in minutes + - recipe.cook_time_minutes: Cooking time in minutes + - recipe.skill_level: One of "beginner", "intermediate", or "advanced" + + Always include all fields in the response. Be creative and helpful. + """); + +// Wrap with state management middleware +AIAgent agent = new SharedStateAgent(baseAgent, jsonOptions.SerializerOptions); + +// Map the AG-UI agent endpoint +app.MapAGUI("/", agent); + +await app.RunAsync(); diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Properties/launchSettings.json b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Properties/launchSettings.json new file mode 100644 index 0000000000..2bac1b9426 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7047;http://localhost:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/RecipeModels.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/RecipeModels.cs new file mode 100644 index 0000000000..46437d78b1 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/RecipeModels.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace RecipeAssistant; + +// State wrapper +internal sealed class AgentState +{ + [JsonPropertyName("recipe")] + public RecipeState Recipe { get; set; } = new(); +} + +// Recipe state model +internal sealed class RecipeState +{ + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("cuisine")] + public string Cuisine { get; set; } = string.Empty; + + [JsonPropertyName("ingredients")] + public List Ingredients { get; set; } = []; + + [JsonPropertyName("steps")] + public List Steps { get; set; } = []; + + [JsonPropertyName("prep_time_minutes")] + public int PrepTimeMinutes { get; set; } + + [JsonPropertyName("cook_time_minutes")] + public int CookTimeMinutes { get; set; } + + [JsonPropertyName("skill_level")] + public string SkillLevel { get; set; } = string.Empty; +} + +// JSON serialization context +[JsonSerializable(typeof(AgentState))] +[JsonSerializable(typeof(RecipeState))] +[JsonSerializable(typeof(System.Text.Json.JsonElement))] +internal sealed partial class RecipeSerializerContext : JsonSerializerContext; diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj new file mode 100644 index 0000000000..d4fc73a066 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs new file mode 100644 index 0000000000..cb11749ddb --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using System.Text.Json; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +namespace RecipeAssistant; + +internal sealed class SharedStateAgent : DelegatingAIAgent +{ + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions) + : base(innerAgent) + { + this._jsonSerializerOptions = jsonSerializerOptions; + } + + public override Task RunAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + return this.RunStreamingAsync(messages, thread, options, cancellationToken) + .ToAgentRunResponseAsync(cancellationToken); + } + + public override async IAsyncEnumerable RunStreamingAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Check if the client sent state in the request + if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions || + !properties.TryGetValue("ag_ui_state", out object? stateObj) || + stateObj is not JsonElement state || + state.ValueKind != JsonValueKind.Object) + { + // No state management requested, pass through to inner agent + await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + yield break; + } + + // Check if state has properties (not empty {}) + bool hasProperties = false; + foreach (JsonProperty _ in state.EnumerateObject()) + { + hasProperties = true; + break; + } + + if (!hasProperties) + { + // Empty state - treat as no state + await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + yield break; + } + + // First run: Generate structured state update + 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( + schemaName: "AgentState", + schemaDescription: "A response containing a recipe with title, skill level, cooking time, ingredients, and instructions"); + + // Add current state to the conversation - state is already a JsonElement + Microsoft.Extensions.AI.ChatMessage stateUpdateMessage = new( + Microsoft.Extensions.AI.ChatRole.System, + [ + new TextContent("Here is the current state in JSON format:"), + new TextContent(JsonSerializer.Serialize(state, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))), + new TextContent("The new state is:") + ]); + + var firstRunMessages = messages.Append(stateUpdateMessage); + + // Collect all updates from first run + var allUpdates = new List(); + 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(); + + // Try to deserialize the structured state response + if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot)) + { + // Serialize and emit as STATE_SNAPSHOT via DataContent + byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes( + stateSnapshot, + this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement))); + yield return new AgentRunResponseUpdate + { + Contents = [new DataContent(stateBytes, "application/json")] + }; + } + else + { + yield break; + } + + // Second run: Generate user-friendly summary + var secondRunMessages = messages.Concat(response.Messages).Append( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.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; + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/appsettings.Development.json b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/appsettings.json b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/test-client.ps1 new file mode 100644 index 0000000000..d2abd889e7 --- /dev/null +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/test-client.ps1 @@ -0,0 +1,10 @@ +$env:AGUI_SERVER_URL="http://localhost:8888" +Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" + +# Test with a simple question +$input = @" +What is 2 + 2? +quit +"@ + +$input | dotnet run From 6f924aa01f1aa91590d0ec14e1b072153009d7c2 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 13 Nov 2025 23:14:15 +0100 Subject: [PATCH 3/9] Cleanups --- dotnet/agent-framework-dotnet.slnx | 23 +++++++++++++++++++ .../Client/Client.csproj | 6 ++--- .../Step01_GettingStarted/Client/Program.cs | 3 +-- .../Step01_GettingStarted/Server/Program.cs | 2 +- .../Server/Server.csproj | 17 +++++++++----- .../Step02_BackendTools/Client/Client.csproj | 6 ++--- .../Step02_BackendTools/Client/Program.cs | 7 +++--- .../Step02_BackendTools/Server/Program.cs | 4 +--- .../Step02_BackendTools/Server/Server.csproj | 17 +++++++++----- .../Step03_FrontendTools/Client/Client.csproj | 6 ++--- .../Step03_FrontendTools/Client/Program.cs | 3 +-- .../Step03_FrontendTools/Server/Program.cs | 2 +- .../Step03_FrontendTools/Server/Server.csproj | 17 +++++++++----- .../Step04_HumanInLoop/Client/Client.csproj | 4 ++-- .../AGUI/Step04_HumanInLoop/Client/Program.cs | 4 +--- .../ServerFunctionApprovalClientAgent.cs | 23 ++++++++----------- .../AGUI/Step04_HumanInLoop/Server/Program.cs | 7 ++---- .../Step04_HumanInLoop/Server/Server.csproj | 15 ++++++++---- .../ServerFunctionApprovalServerAgent.cs | 20 ++++++++-------- .../Step04_HumanInLoop.slnx | 4 ---- .../AGUI/Step04_HumanInLoop/test-client.ps1 | 10 -------- .../Client/Client.csproj | 6 ++--- .../Step05_StateManagement/Client/Program.cs | 20 ++++++++++++---- .../Client/StatefulAgent.cs | 6 ++--- .../Step05_StateManagement/Server/Program.cs | 2 +- .../Server/RecipeModels.cs | 2 +- .../Server/Server.csproj | 17 +++++++++----- .../Server/SharedStateAgent.cs | 14 +++++------ 28 files changed, 148 insertions(+), 119 deletions(-) delete mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Step04_HumanInLoop.slnx delete mode 100644 dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/test-client.ps1 diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 908884476d..254b549794 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -77,6 +77,29 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj index 8c996da81d..1b02976cad 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj @@ -2,14 +2,14 @@ Exe - net10.0 + net9.0 enable enable - - + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs index 20154c3d57..48f1c0bfec 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs @@ -36,7 +36,7 @@ while (true) { string? message; - + if (autoMode) { if (queryIndex >= queries.Count) @@ -112,4 +112,3 @@ { Console.WriteLine($"\nAn error occurred: {ex.Message}"); } - diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Program.cs index f13cdaf8a7..1bfb9a97aa 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj index d4fc73a066..f01019e9f4 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj @@ -1,16 +1,21 @@ - net10.0 - enable + Exe + net9.0 enable + enable - - - - + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj index 8c996da81d..1b02976cad 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj @@ -2,14 +2,14 @@ Exe - net10.0 + net9.0 enable enable - - + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs index b22047bde9..2b587ec9e6 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs @@ -36,7 +36,7 @@ while (true) { string? message; - + if (autoMode) { if (queryIndex >= queries.Count) @@ -99,7 +99,7 @@ case FunctionCallContent functionCallContent: Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"\n[Function Call - Name: {functionCallContent.Name}]"); - + // Display individual parameters if (functionCallContent.Arguments != null) { @@ -114,7 +114,7 @@ case FunctionResultContent functionResultContent: Console.ForegroundColor = ConsoleColor.Magenta; Console.WriteLine($"\n[Function Result - CallId: {functionResultContent.CallId}]"); - + if (functionResultContent.Exception != null) { Console.WriteLine($" Exception: {functionResultContent.Exception}"); @@ -144,4 +144,3 @@ { Console.WriteLine($"\nAn error occurred: {ex.Message}"); } - diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Program.cs index 1f91752b0c..2867721d02 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; using System.Text.Json.Serialization; @@ -115,5 +115,3 @@ internal sealed class RestaurantInfo [JsonSerializable(typeof(RestaurantSearchRequest))] [JsonSerializable(typeof(RestaurantSearchResponse))] internal sealed partial class SampleJsonSerializerContext : JsonSerializerContext; - - diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj index d4fc73a066..f01019e9f4 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj @@ -1,16 +1,21 @@ - net10.0 - enable + Exe + net9.0 enable + enable - - - - + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj index 8c996da81d..1b02976cad 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj @@ -2,14 +2,14 @@ Exe - net10.0 + net9.0 enable enable - - + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs index 2a418bd523..3faf403a08 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs @@ -49,7 +49,7 @@ static string GetUserLocation() while (true) { string? message; - + if (autoMode) { if (queryIndex >= queries.Count) @@ -137,4 +137,3 @@ static string GetUserLocation() { Console.WriteLine($"\nAn error occurred: {ex.Message}"); } - diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Program.cs index f13cdaf8a7..1bfb9a97aa 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj index d4fc73a066..f01019e9f4 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj @@ -1,16 +1,21 @@ - net10.0 - enable + Exe + net9.0 enable + enable - - - - + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj index 30fe7c668f..1b02976cad 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Program.cs index 4d01e7d2d1..656989458d 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Program.cs @@ -1,8 +1,6 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. -using System.Runtime.CompilerServices; using System.Text.Json; -using System.Text.Json.Serialization; using Microsoft.Agents.AI; using Microsoft.Agents.AI.AGUI; using Microsoft.Extensions.AI; diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs index c94c5ea79c..709cb5bb7f 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Runtime.CompilerServices; using System.Text.Json; @@ -8,7 +8,7 @@ /// /// A delegating agent that handles server function approval requests and responses. -/// Transforms between FunctionApprovalRequestContent/FunctionApprovalResponseContent +/// Transforms between FunctionApprovalRequestContent/FunctionApprovalResponseContent /// and the server's request_approval tool call pattern. /// internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent @@ -18,7 +18,7 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent public ServerFunctionApprovalClientAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions) : base(innerAgent) { - _jsonSerializerOptions = jsonSerializerOptions; + this._jsonSerializerOptions = jsonSerializerOptions; } public override Task RunAsync( @@ -27,7 +27,7 @@ public override Task RunAsync( AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - return RunStreamingAsync(messages, thread, options, cancellationToken) + return this.RunStreamingAsync(messages, thread, options, cancellationToken) .ToAgentRunResponseAsync(cancellationToken); } @@ -38,13 +38,13 @@ public override async IAsyncEnumerable RunStreamingAsync [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Process and transform approval messages, creating a new message list - var processedMessages = ProcessOutgoingServerFunctionApprovals(messages, _jsonSerializerOptions); + var processedMessages = ProcessOutgoingServerFunctionApprovals(messages, this._jsonSerializerOptions); // Run the inner agent and intercept any approval requests - await foreach (var update in InnerAgent.RunStreamingAsync( + await foreach (var update in this.InnerAgent.RunStreamingAsync( processedMessages, thread, options, cancellationToken).ConfigureAwait(false)) { - yield return ProcessIncomingServerApprovalRequests(update, _jsonSerializerOptions); + yield return ProcessIncomingServerApprovalRequests(update, this._jsonSerializerOptions); } } @@ -137,9 +137,8 @@ private static IEnumerable ProcessOutgoingServerFunctionApprovals( } } - if (transformedContents != null && transformedContents.Count == 0) + if (transformedContents?.Count == 0) { - transformedContents = null; // Entire message is skipped continue; } else if (transformedContents != null) @@ -232,10 +231,8 @@ private static AgentRunResponseUpdate ProcessIncomingServerApprovalRequests( ContinuationToken = update.ContinuationToken, }; } - else - { - return update; - } + + return update; } } #pragma warning restore MEAI001 diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs index 5183f88b13..6cd75a7713 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -1,5 +1,6 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; @@ -9,10 +10,6 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Options; using OpenAI.Chat; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj index a25b01e59d..f01019e9f4 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj @@ -1,16 +1,21 @@ + Exe net9.0 - enable enable + enable - - - - + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs index d4f50a7373..93e721605e 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Runtime.CompilerServices; using System.Text.Json; @@ -18,7 +18,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent public ServerFunctionApprovalAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions) : base(innerAgent) { - _jsonSerializerOptions = jsonSerializerOptions; + this._jsonSerializerOptions = jsonSerializerOptions; } public override Task RunAsync( @@ -27,7 +27,7 @@ public override Task RunAsync( AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - return RunStreamingAsync(messages, thread, options, cancellationToken) + return this.RunStreamingAsync(messages, thread, options, cancellationToken) .ToAgentRunResponseAsync(cancellationToken); } @@ -38,13 +38,13 @@ public override async IAsyncEnumerable RunStreamingAsync [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Process and transform incoming approval responses from client, creating a new message list - var processedMessages = ProcessIncomingFunctionApprovals(messages, _jsonSerializerOptions); + var processedMessages = ProcessIncomingFunctionApprovals(messages, this._jsonSerializerOptions); // Run the inner agent and intercept any approval requests - await foreach (var update in InnerAgent.RunStreamingAsync( + await foreach (var update in this.InnerAgent.RunStreamingAsync( processedMessages, thread, options, cancellationToken).ConfigureAwait(false)) { - yield return ProcessOutgoingApprovalRequests(update, _jsonSerializerOptions); + yield return ProcessOutgoingApprovalRequests(update, this._jsonSerializerOptions); } } @@ -119,7 +119,7 @@ private static IList ProcessIncomingFunctionApprovals( List? result = null; // Track approval ID to original call ID mapping - var approvalIdToCallId = new Dictionary(); + _ = new Dictionary(); #pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. Dictionary trackedRequestApprovalToolCalls = new(); // Remote approvals for (int messageIndex = 0; messageIndex < messagesList.Count; messageIndex++) @@ -223,10 +223,8 @@ private static AgentRunResponseUpdate ProcessOutgoingApprovalRequests( ContinuationToken = update.ContinuationToken }; } - else - { - return update; - } + + return update; } } diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Step04_HumanInLoop.slnx b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Step04_HumanInLoop.slnx deleted file mode 100644 index 969c14884b..0000000000 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Step04_HumanInLoop.slnx +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/test-client.ps1 deleted file mode 100644 index d2abd889e7..0000000000 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/test-client.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$env:AGUI_SERVER_URL="http://localhost:8888" -Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" - -# Test with a simple question -$input = @" -What is 2 + 2? -quit -"@ - -$input | dotnet run diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj index 8c996da81d..1b02976cad 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj @@ -2,14 +2,14 @@ Exe - net10.0 + net9.0 enable enable - - + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs index 3235c5199d..b905a33578 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs @@ -45,7 +45,7 @@ while (true) { string? message; - + if (queries.Length > 0) { if (queryIndex >= queries.Length) @@ -173,20 +173,31 @@ static void DisplayState(RecipeState? state) if (!string.IsNullOrEmpty(state.Title)) { - Console.WriteLine($"\nRecipe:"); + Console.WriteLine("\nRecipe:"); Console.WriteLine($" Title: {state.Title}"); if (!string.IsNullOrEmpty(state.Cuisine)) + { Console.WriteLine($" Cuisine: {state.Cuisine}"); + } + if (!string.IsNullOrEmpty(state.SkillLevel)) + { Console.WriteLine($" Skill Level: {state.SkillLevel}"); + } + if (state.PrepTimeMinutes > 0) + { Console.WriteLine($" Prep Time: {state.PrepTimeMinutes} minutes"); + } + if (state.CookTimeMinutes > 0) + { Console.WriteLine($" Cook Time: {state.CookTimeMinutes} minutes"); + } if (state.Ingredients.Count > 0) { - Console.WriteLine($"\n Ingredients:"); + Console.WriteLine("\n Ingredients:"); foreach (var ingredient in state.Ingredients) { Console.WriteLine($" - {ingredient}"); @@ -195,7 +206,7 @@ static void DisplayState(RecipeState? state) if (state.Steps.Count > 0) { - Console.WriteLine($"\n Steps:"); + Console.WriteLine("\n Steps:"); for (int i = 0; i < state.Steps.Count; i++) { Console.WriteLine($" {i + 1}. {state.Steps[i]}"); @@ -245,4 +256,3 @@ internal sealed class RecipeState [JsonSerializable(typeof(RecipeState))] [JsonSerializable(typeof(JsonElement))] internal sealed partial class RecipeSerializerContext : JsonSerializerContext; - diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs index 8e93b9ac25..8321efaa73 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/StatefulAgent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Runtime.CompilerServices; using System.Text.Json; @@ -11,7 +11,7 @@ namespace RecipeClient; /// A delegating agent that manages client-side state and automatically attaches it to requests. /// /// The state type. -internal sealed class StatefulAgent : DelegatingAIAgent +internal sealed class StatefulAgent : DelegatingAIAgent where TState : class, new() { private readonly JsonSerializerOptions _jsonSerializerOptions; @@ -54,7 +54,7 @@ public override async IAsyncEnumerable RunStreamingAsync { // Add state to messages List messagesWithState = [.. messages]; - + // Serialize the state using AgentState wrapper byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes( this.State, diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Program.cs index 9139fef598..40c51887d1 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using Azure.AI.OpenAI; using Azure.Identity; diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/RecipeModels.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/RecipeModels.cs index 46437d78b1..fc1d8320d2 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/RecipeModels.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/RecipeModels.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj index d4fc73a066..f01019e9f4 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj @@ -1,16 +1,21 @@ - net10.0 - enable + Exe + net9.0 enable + enable - - - - + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs index cb11749ddb..4588c7bd60 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Runtime.CompilerServices; using System.Text.Json; @@ -18,7 +18,7 @@ public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializer } public override Task RunAsync( - IEnumerable messages, + IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) @@ -28,7 +28,7 @@ public override Task RunAsync( } public override async IAsyncEnumerable RunStreamingAsync( - IEnumerable messages, + IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -80,8 +80,8 @@ stateObj is not JsonElement state || schemaDescription: "A response containing a recipe with title, skill level, cooking time, ingredients, and instructions"); // Add current state to the conversation - state is already a JsonElement - Microsoft.Extensions.AI.ChatMessage stateUpdateMessage = new( - Microsoft.Extensions.AI.ChatRole.System, + ChatMessage stateUpdateMessage = new( + ChatRole.System, [ new TextContent("Here is the current state in JSON format:"), new TextContent(JsonSerializer.Serialize(state, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))), @@ -125,8 +125,8 @@ stateObj is not JsonElement state || // Second run: Generate user-friendly summary var secondRunMessages = messages.Concat(response.Messages).Append( - new Microsoft.Extensions.AI.ChatMessage( - Microsoft.Extensions.AI.ChatRole.System, + 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)) From beabff3eee57953eb8ca56d1941feb7ee93b4d07 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 14 Nov 2025 12:20:41 +0100 Subject: [PATCH 4/9] Update the dojo samples --- .../AGUIDojoServerSerializerContext.cs | 12 ++ .../AgenticUI/AgenticPlanningTools.cs | 52 +++++++++ .../AgenticUI/AgenticUIAgent.cs | 88 +++++++++++++++ .../AgenticUI/JsonPatchOperation.cs | 20 ++++ .../AGUIDojoServer/AgenticUI/Plan.cs | 11 ++ .../AGUIDojoServer/AgenticUI/Step.cs | 14 +++ .../AGUIDojoServer/AgenticUI/StepStatus.cs | 12 ++ .../{ => BackendToolRendering}/WeatherInfo.cs | 4 +- .../AGUIDojoServer/ChatClientAgentFactory.cs | 93 +++++++++++++++- .../PredictiveStateUpdates/DocumentState.cs | 11 ++ .../PredictiveStateUpdatesAgent.cs | 104 ++++++++++++++++++ .../AGUIDojoServer/Program.cs | 6 +- .../{ => SharedState}/Ingredient.cs | 4 +- .../{ => SharedState}/Recipe.cs | 4 +- .../{ => SharedState}/RecipeResponse.cs | 4 +- .../{ => SharedState}/SharedStateAgent.cs | 4 +- .../Properties/launchSettings.json | 12 ++ 17 files changed, 438 insertions(+), 17 deletions(-) create mode 100644 dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs create mode 100644 dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs create mode 100644 dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs create mode 100644 dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs create mode 100644 dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs create mode 100644 dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs rename dotnet/samples/AGUIClientServer/AGUIDojoServer/{ => BackendToolRendering}/WeatherInfo.cs (83%) create mode 100644 dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs create mode 100644 dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs rename dotnet/samples/AGUIClientServer/AGUIDojoServer/{ => SharedState}/Ingredient.cs (79%) rename dotnet/samples/AGUIClientServer/AGUIDojoServer/{ => SharedState}/Recipe.cs (88%) rename dotnet/samples/AGUIClientServer/AGUIDojoServer/{ => SharedState}/RecipeResponse.cs (75%) rename dotnet/samples/AGUIClientServer/AGUIDojoServer/{ => SharedState}/SharedStateAgent.cs (98%) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServerSerializerContext.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServerSerializerContext.cs index af86dc2598..c60db0efd0 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServerSerializerContext.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServerSerializerContext.cs @@ -1,6 +1,10 @@ // 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; @@ -8,4 +12,12 @@ namespace AGUIDojoServer; [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))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(DocumentState))] internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs new file mode 100644 index 0000000000..26803e3fc7 --- /dev/null +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs @@ -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 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> 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(); + + 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; + } +} diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs new file mode 100644 index 0000000000..05a7d86f15 --- /dev/null +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs @@ -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; + } + + public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken); + } + + public override async IAsyncEnumerable RunStreamingAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Track function calls that should trigger state events + var trackedFunctionCalls = new Dictionary(); + + 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 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 AgentRunResponseUpdate( + 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 + }; + } + } +} diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs new file mode 100644 index 0000000000..f4e531c8e4 --- /dev/null +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs @@ -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; } +} diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs new file mode 100644 index 0000000000..26a2010ab3 --- /dev/null +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs @@ -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 Steps { get; set; } = []; +} diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs new file mode 100644 index 0000000000..53852f9232 --- /dev/null +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs @@ -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; +} diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs new file mode 100644 index 0000000000..878cfa5476 --- /dev/null +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace AGUIDojoServer.AgenticUI; + +[JsonConverter(typeof(JsonStringEnumConverter))] +internal enum StepStatus +{ + Pending, + Completed +} diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/WeatherInfo.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs similarity index 83% rename from dotnet/samples/AGUIClientServer/AGUIDojoServer/WeatherInfo.cs rename to dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs index e5b4811739..fd17ac0d26 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/WeatherInfo.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs @@ -1,8 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; -namespace AGUIDojoServer; +namespace AGUIDojoServer.BackendToolRendering; internal sealed class WeatherInfo { diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs index 5145c5559d..630bc099ce 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs @@ -2,10 +2,15 @@ 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 OpenAI; using ChatClient = OpenAI.Chat.ChatClient; namespace AGUIDojoServer; @@ -66,13 +71,46 @@ public static ChatClientAgent CreateToolBasedGenerativeUI() description: "An agent that uses tools to generate user interfaces using Azure OpenAI"); } - public static ChatClientAgent CreateAgenticUI() + public static AIAgent CreateAgenticUI(JsonSerializerOptions options) { ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!); - - return chatClient.AsIChatClient().CreateAIAgent( - name: "AgenticUIAgent", - description: "An agent that generates agentic user interfaces using Azure OpenAI"); + var baseAgent = chatClient.AsIChatClient().CreateAIAgent(new ChatClientAgentOptions + { + Name = "AgenticUIAgent", + Description = "An agent that generates agentic user interfaces using Azure OpenAI", + 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. + """, + ChatOptions = new ChatOptions + { + 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) @@ -86,6 +124,44 @@ public static AIAgent CreateSharedState(JsonSerializerOptions options) return new SharedStateAgent(baseAgent, options); } + public static AIAgent CreatePredictiveStateUpdates(JsonSerializerOptions options) + { + ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!); + + var baseAgent = chatClient.AsIChatClient().CreateAIAgent(new ChatClientAgentOptions + { + Name = "PredictiveStateUpdatesAgent", + Description = "An agent that demonstrates predictive state updates using Azure OpenAI", + 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. + """, + ChatOptions = new ChatOptions + { + 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() { @@ -95,4 +171,11 @@ public static AIAgent CreateSharedState(JsonSerializerOptions options) 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"; + } } diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs new file mode 100644 index 0000000000..0138fcde66 --- /dev/null +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs @@ -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; +} diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs new file mode 100644 index 0000000000..8ac9928fbe --- /dev/null +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs @@ -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; + } + + public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken); + } + + public override async IAsyncEnumerable RunStreamingAsync( + IEnumerable 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 AgentRunResponseUpdate( + 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; + } + } + } +} diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs index 57cc409c58..a0d8dacf97 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs @@ -35,11 +35,13 @@ app.MapAGUI("/tool_based_generative_ui", ChatClientAgentFactory.CreateToolBasedGenerativeUI()); -app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI()); - var jsonOptions = app.Services.GetRequiredService>(); +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 { } diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/Ingredient.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs similarity index 79% rename from dotnet/samples/AGUIClientServer/AGUIDojoServer/Ingredient.cs rename to dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs index 4be57405ae..8ba98894bf 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/Ingredient.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs @@ -1,8 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; -namespace AGUIDojoServer; +namespace AGUIDojoServer.SharedState; internal sealed class Ingredient { diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/Recipe.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs similarity index 88% rename from dotnet/samples/AGUIClientServer/AGUIDojoServer/Recipe.cs rename to dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs index 9af4f6eae9..f01ff53d91 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/Recipe.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs @@ -1,8 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; -namespace AGUIDojoServer; +namespace AGUIDojoServer.SharedState; internal sealed class Recipe { diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/RecipeResponse.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs similarity index 75% rename from dotnet/samples/AGUIClientServer/AGUIDojoServer/RecipeResponse.cs rename to dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs index 0e9b2f2fff..1f316bf03f 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/RecipeResponse.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs @@ -1,8 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; -namespace AGUIDojoServer; +namespace AGUIDojoServer.SharedState; #pragma warning disable CA1812 // Used for the JsonSchema response format internal sealed class RecipeResponse diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedStateAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs similarity index 98% rename from dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedStateAgent.cs rename to dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs index ea2f1d319f..484f9c8824 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedStateAgent.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -6,7 +6,7 @@ using Microsoft.Agents.AI; using Microsoft.Extensions.AI; -namespace AGUIDojoServer; +namespace AGUIDojoServer.SharedState; [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateSharedState")] internal sealed class SharedStateAgent : DelegatingAIAgent diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json new file mode 100644 index 0000000000..ccadf94b3a --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Microsoft.Agents.AI.Hosting.A2A.UnitTests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:55034;http://localhost:55035" + } + } +} \ No newline at end of file From 660dee85a0f9f8c7c12c25efb0d38c33e76bcf34 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Mon, 17 Nov 2025 13:42:12 +0100 Subject: [PATCH 5/9] cleanups --- .../AgenticUI/AgenticPlanningTools.cs | 2 +- .../AgenticUI/JsonPatchOperation.cs | 2 +- .../AGUIDojoServer/AgenticUI/Plan.cs | 2 +- .../AGUIDojoServer/AgenticUI/Step.cs | 2 +- .../AGUIDojoServer/AgenticUI/StepStatus.cs | 2 +- .../BackendToolRendering/WeatherInfo.cs | 2 +- .../PredictiveStateUpdates/DocumentState.cs | 2 +- .../AGUIDojoServer/SharedState/Ingredient.cs | 2 +- .../AGUIDojoServer/SharedState/Recipe.cs | 2 +- .../SharedState/RecipeResponse.cs | 2 +- .../SharedState/SharedStateAgent.cs | 2 +- dotnet/samples/AGUIWebChat/Client/Program.cs | 2 + dotnet/samples/AGUIWebChat/Server/Program.cs | 4 +- .../ServerFunctionApprovalClientAgent.cs | 53 ++++++++------- .../AGUI/Step04_HumanInLoop/Server/Program.cs | 1 + .../ServerFunctionApprovalServerAgent.cs | 67 ++++++++++--------- 16 files changed, 79 insertions(+), 70 deletions(-) diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs index 26803e3fc7..98fe96b442 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs index f4e531c8e4..1cd8f5dcd2 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs index 26a2010ab3..a8ffcc6c37 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs index 53852f9232..26bc9860a5 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs index 878cfa5476..f88d71bef0 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs index fd17ac0d26..d6e3be9b80 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs index 0138fcde66..ad053fe4a2 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs index 8ba98894bf..d56d88d958 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs index f01ff53d91..a8485da839 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs index 1f316bf03f..dadf3b7a2b 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs index 484f9c8824..c10450fcfb 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; diff --git a/dotnet/samples/AGUIWebChat/Client/Program.cs b/dotnet/samples/AGUIWebChat/Client/Program.cs index 22a2c11388..c145227062 100644 --- a/dotnet/samples/AGUIWebChat/Client/Program.cs +++ b/dotnet/samples/AGUIWebChat/Client/Program.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + using AGUIWebChatClient.Components; using Microsoft.Agents.AI.AGUI; diff --git a/dotnet/samples/AGUIWebChat/Server/Program.cs b/dotnet/samples/AGUIWebChat/Server/Program.cs index 8c899353b3..1683a7e3ed 100644 --- a/dotnet/samples/AGUIWebChat/Server/Program.cs +++ b/dotnet/samples/AGUIWebChat/Server/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // This sample demonstrates a basic AG-UI server hosting a chat agent for the Blazor web client. @@ -19,7 +19,7 @@ string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); // Create the AI agent -AzureOpenAIClient azureOpenAIClient = new AzureOpenAIClient( +AzureOpenAIClient azureOpenAIClient = new( new Uri(endpoint), new DefaultAzureCredential()); diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs index 709cb5bb7f..acaa888309 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using ServerFunctionApproval; /// /// A delegating agent that handles server function approval requests and responses. @@ -38,7 +39,7 @@ public override async IAsyncEnumerable RunStreamingAsync [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Process and transform approval messages, creating a new message list - var processedMessages = ProcessOutgoingServerFunctionApprovals(messages, this._jsonSerializerOptions); + var processedMessages = ProcessOutgoingServerFunctionApprovals(messages.ToList(), this._jsonSerializerOptions); // Run the inner agent and intercept any approval requests await foreach (var update in this.InnerAgent.RunStreamingAsync( @@ -62,7 +63,7 @@ private static FunctionResultContent ConvertApprovalResponseToToolResult(Functio jsonOptions)); } - private static List CopyMessagesUpToIndex(IList messages, int index) + private static List CopyMessagesUpToIndex(List messages, int index) { var result = new List(index); for (int i = 0; i < index; i++) @@ -82,17 +83,16 @@ private static List CopyContentsUpToIndex(IList contents, return result; } - private static IEnumerable ProcessOutgoingServerFunctionApprovals( - IEnumerable messages, + private static List ProcessOutgoingServerFunctionApprovals( + List messages, JsonSerializerOptions jsonSerializerOptions) { - var messagesList = messages.ToList(); List? result = null; Dictionary approvalRequests = []; - for (var messageIndex = 0; messageIndex < messagesList.Count; messageIndex++) + for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++) { - var message = messagesList[messageIndex]; + var message = messages[messageIndex]; List? transformedContents = null; // Process each content item in the message @@ -152,7 +152,7 @@ private static IEnumerable ProcessOutgoingServerFunctionApprovals( RawRepresentation = message.RawRepresentation, AdditionalProperties = message.AdditionalProperties }; - result ??= CopyMessagesUpToIndex(messagesList, messageIndex); + result ??= CopyMessagesUpToIndex(messages, messageIndex); result.Add(newMessage); } else if (result != null) @@ -163,7 +163,7 @@ private static IEnumerable ProcessOutgoingServerFunctionApprovals( // If result is null, we haven't made any changes yet, so keep processing } - return result ?? messagesList; + return result ?? messages; } private static AgentRunResponseUpdate ProcessIncomingServerApprovalRequests( @@ -237,26 +237,29 @@ private static AgentRunResponseUpdate ProcessIncomingServerApprovalRequests( } #pragma warning restore MEAI001 -public sealed class ApprovalRequest +namespace ServerFunctionApproval { - [JsonPropertyName("approval_id")] - public required string ApprovalId { get; init; } + public sealed class ApprovalRequest + { + [JsonPropertyName("approval_id")] + public required string ApprovalId { get; init; } - [JsonPropertyName("function_name")] - public required string FunctionName { get; init; } + [JsonPropertyName("function_name")] + public required string FunctionName { get; init; } - [JsonPropertyName("function_arguments")] - public JsonElement? FunctionArguments { get; init; } + [JsonPropertyName("function_arguments")] + public JsonElement? FunctionArguments { get; init; } - [JsonPropertyName("message")] - public string? Message { get; init; } -} + [JsonPropertyName("message")] + public string? Message { get; init; } + } -public sealed class ApprovalResponse -{ - [JsonPropertyName("approval_id")] - public required string ApprovalId { get; init; } + public sealed class ApprovalResponse + { + [JsonPropertyName("approval_id")] + public required string ApprovalId { get; init; } - [JsonPropertyName("approved")] - public required bool Approved { get; init; } + [JsonPropertyName("approved")] + public required bool Approved { get; init; } + } } diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs index 6cd75a7713..1af163435a 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Options; using OpenAI.Chat; +using ServerFunctionApproval; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs index 93e721605e..f515e97531 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/ServerFunctionApprovalServerAgent.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using ServerFunctionApproval; /// /// A delegating agent that handles function approval requests on the server side. @@ -38,7 +39,7 @@ public override async IAsyncEnumerable RunStreamingAsync [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Process and transform incoming approval responses from client, creating a new message list - var processedMessages = ProcessIncomingFunctionApprovals(messages, this._jsonSerializerOptions); + var processedMessages = ProcessIncomingFunctionApprovals(messages.ToList(), this._jsonSerializerOptions); // Run the inner agent and intercept any approval requests await foreach (var update in this.InnerAgent.RunStreamingAsync( @@ -91,7 +92,7 @@ result.Result is string str ? } #pragma warning restore MEAI001 - private static List CopyMessagesUpToIndex(IList messages, int index) + private static List CopyMessagesUpToIndex(List messages, int index) { var result = new List(index); for (int i = 0; i < index; i++) @@ -111,27 +112,26 @@ private static List CopyContentsUpToIndex(IList contents, return result; } - private static IList ProcessIncomingFunctionApprovals( - IEnumerable messages, + private static List ProcessIncomingFunctionApprovals( + List messages, JsonSerializerOptions jsonSerializerOptions) { - var messagesList = messages.ToList(); List? result = null; // Track approval ID to original call ID mapping _ = new Dictionary(); #pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. Dictionary trackedRequestApprovalToolCalls = new(); // Remote approvals - for (int messageIndex = 0; messageIndex < messagesList.Count; messageIndex++) + for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++) { - var message = messagesList[messageIndex]; + var message = messages[messageIndex]; List? transformedContents = null; for (int j = 0; j < message.Contents.Count; j++) { var content = message.Contents[j]; if (content is FunctionCallContent { Name: "request_approval" } toolCall) { - result ??= CopyMessagesUpToIndex(messagesList, messageIndex); + result ??= CopyMessagesUpToIndex(messages, messageIndex); transformedContents ??= CopyContentsUpToIndex(message.Contents, j); var approvalRequest = ConvertToolCallToApprovalRequest(toolCall, jsonSerializerOptions); transformedContents.Add(approvalRequest); @@ -148,7 +148,7 @@ private static IList ProcessIncomingFunctionApprovals( else if (content is FunctionResultContent toolResult && trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval) == true) { - result ??= CopyMessagesUpToIndex(messagesList, messageIndex); + result ??= CopyMessagesUpToIndex(messages, messageIndex); transformedContents ??= CopyContentsUpToIndex(message.Contents, j); var approvalResponse = ConvertToolResultToApprovalResponse(toolResult, approval, jsonSerializerOptions); transformedContents.Add(approvalResponse); @@ -169,7 +169,7 @@ private static IList ProcessIncomingFunctionApprovals( } #pragma warning restore MEAI001 - return result ?? messagesList; + return result ?? messages; } private static AgentRunResponseUpdate ProcessOutgoingApprovalRequests( @@ -228,32 +228,35 @@ private static AgentRunResponseUpdate ProcessOutgoingApprovalRequests( } } -// Define approval models -public sealed class ApprovalRequest +namespace ServerFunctionApproval { - [JsonPropertyName("approval_id")] - public required string ApprovalId { get; init; } + // Define approval models + public sealed class ApprovalRequest + { + [JsonPropertyName("approval_id")] + public required string ApprovalId { get; init; } - [JsonPropertyName("function_name")] - public required string FunctionName { get; init; } + [JsonPropertyName("function_name")] + public required string FunctionName { get; init; } - [JsonPropertyName("function_arguments")] - public IDictionary? FunctionArguments { get; init; } + [JsonPropertyName("function_arguments")] + public IDictionary? FunctionArguments { get; init; } - [JsonPropertyName("message")] - public string? Message { get; init; } -} + [JsonPropertyName("message")] + public string? Message { get; init; } + } -public sealed class ApprovalResponse -{ - [JsonPropertyName("approval_id")] - public required string ApprovalId { get; init; } + public sealed class ApprovalResponse + { + [JsonPropertyName("approval_id")] + public required string ApprovalId { get; init; } - [JsonPropertyName("approved")] - public required bool Approved { get; init; } -} + [JsonPropertyName("approved")] + public required bool Approved { get; init; } + } -[JsonSerializable(typeof(ApprovalRequest))] -[JsonSerializable(typeof(ApprovalResponse))] -[JsonSerializable(typeof(Dictionary))] -public sealed partial class ApprovalJsonContext : JsonSerializerContext; + [JsonSerializable(typeof(ApprovalRequest))] + [JsonSerializable(typeof(ApprovalResponse))] + [JsonSerializable(typeof(Dictionary))] + public sealed partial class ApprovalJsonContext : JsonSerializerContext; +} From 2f620cd2bb28aa1915b136c266e881c94c116649 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Mon, 17 Nov 2025 14:02:51 +0100 Subject: [PATCH 6/9] Fix readme --- dotnet/samples/GettingStarted/AGUI/README.md | 144 +++++++++++++------ 1 file changed, 102 insertions(+), 42 deletions(-) diff --git a/dotnet/samples/GettingStarted/AGUI/README.md b/dotnet/samples/GettingStarted/AGUI/README.md index a65ecf1a1d..a624fe81f3 100644 --- a/dotnet/samples/GettingStarted/AGUI/README.md +++ b/dotnet/samples/GettingStarted/AGUI/README.md @@ -18,7 +18,7 @@ export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" ``` -For the client sample, you can optionally set: +For the client samples, you can optionally set: ```bash export AGUI_SERVER_URL="http://localhost:8888" @@ -26,7 +26,11 @@ export AGUI_SERVER_URL="http://localhost:8888" ## Samples -### AGUI_Step01_ServerBasic +### Step01_GettingStarted + +A basic AG-UI server and client that demonstrate the foundational concepts. + +#### Server (`Step01_GettingStarted/Server`) A basic AG-UI server that hosts an AI agent accessible via HTTP. Demonstrates: @@ -35,109 +39,165 @@ A basic AG-UI server that hosts an AI agent accessible via HTTP. Demonstrates: - Creating an AI agent from an Azure OpenAI chat client - Streaming responses via Server-Sent Events (SSE) -**Run the sample:** +**Run the server:** ```bash -cd AGUI_Step01_ServerBasic +cd Step01_GettingStarted/Server dotnet run --urls http://localhost:8888 ``` -### AGUI_Step02_ClientBasic +#### Client (`Step01_GettingStarted/Client`) An interactive console client that connects to an AG-UI server. Demonstrates: -- Creating an AG-UI client with `AGUIAgent` +- Creating an AG-UI client with `AGUIChatClient` - Managing conversation threads - Streaming responses with `RunStreamingAsync` - Displaying colored console output for different content types -- Using System.CommandLine for interactive input +- Supporting both interactive and automated modes -**Prerequisites:** AGUI_Step01_ServerBasic (or any AG-UI server) must be running. +**Prerequisites:** The Step01_GettingStarted server (or any AG-UI server) must be running. -**Run the sample:** +**Run the client:** ```bash -cd AGUI_Step02_ClientBasic +cd Step01_GettingStarted/Client dotnet run ``` Type messages and press Enter to interact with the agent. Type `:q` or `quit` to exit. -### AGUI_Step03_ServerWithTools +### Step02_BackendTools + +An AG-UI server with function tools that execute on the backend. -An AG-UI server with function tools that execute on the backend. Demonstrates: +#### Server (`Step02_BackendTools/Server`) + +Demonstrates: - Creating function tools using `AIFunctionFactory.Create` - Using `[Description]` attributes for tool documentation - Defining explicit request/response types for type safety - Setting up JSON serialization contexts for source generation -- Handling both simple (string) and complex (object) return types +- Backend tool rendering (tools execute on the server) -**Run the sample:** +**Run the server:** ```bash -cd AGUI_Step03_ServerWithTools +cd Step02_BackendTools/Server dotnet run --urls http://localhost:8888 ``` -This server can be used with the AGUI_Step02_ClientBasic client. Try asking about weather or searching for restaurants. +#### Client (`Step02_BackendTools/Client`) -### AGUI_Step04_ServerWithState +A client that works with the backend tools server. Try asking: "Find Italian restaurants in Seattle" or "Search for Mexican food in Portland". -An AG-UI server that demonstrates state management with predictive updates. Demonstrates: +**Run the client:** -- Defining state schemas using C# records -- Streaming predictive state updates as tools execute -- Managing shared state between client and server -- Using JSON serialization contexts for state types +```bash +cd Step02_BackendTools/Client +dotnet run +``` + +### Step03_FrontendTools + +Demonstrates frontend tool rendering (tools defined on client, executed on server). -**Run the sample:** +#### Server (`Step03_FrontendTools/Server`) + +A basic AG-UI server that accepts tool definitions from the client. + +**Run the server:** ```bash -cd AGUI_Step04_ServerWithState +cd Step03_FrontendTools/Server dotnet run --urls http://localhost:8888 ``` -### AGUI_Step05_Approvals +#### Client (`Step03_FrontendTools/Client`) + +A client that defines and sends tools to the server for execution. + +**Run the client:** + +```bash +cd Step03_FrontendTools/Client +dotnet run +``` + +### Step04_HumanInLoop Demonstrates human-in-the-loop approval workflows for sensitive operations. This sample includes both a server and client component. -#### Server (`AGUI_Step05_Approvals/Server`) +#### Server (`Step04_HumanInLoop/Server`) An AG-UI server that implements approval workflows. Demonstrates: - Wrapping tools with `ApprovalRequiredAIFunction` -- Converting `FunctionApprovalRequestContent` to client tool calls -- Middleware pattern for approval request/response handling +- Converting `FunctionApprovalRequestContent` to approval requests +- Middleware pattern with `ServerFunctionApprovalServerAgent` - Complete function call capture and restoration **Run the server:** ```bash -cd AGUI_Step05_Approvals/Server +cd Step04_HumanInLoop/Server dotnet run --urls http://localhost:8888 ``` -#### Client (`AGUI_Step05_Approvals/Client`) +#### Client (`Step04_HumanInLoop/Client`) An interactive client that handles approval requests from the server. Demonstrates: -- Detecting and parsing `"request_approval"` tool calls +- Using `ServerFunctionApprovalClientAgent` middleware +- Detecting `FunctionApprovalRequestContent` - Displaying approval details to users - Prompting for approval/rejection -- Sending approval responses as tool results +- Sending approval responses with `FunctionApprovalResponseContent` - Resuming conversation after approval -**Prerequisites:** The approval server must be running. - **Run the client:** ```bash -cd AGUI_Step05_Approvals/Client +cd Step04_HumanInLoop/Client +dotnet run +``` + +Try asking the agent to perform sensitive operations like "Approve expense report EXP-12345". + +### Step05_StateManagement + +An AG-UI server and client that demonstrate state management with predictive updates. + +#### Server (`Step05_StateManagement/Server`) + +Demonstrates: + +- Defining state schemas using C# records +- Using `SharedStateAgent` middleware for state management +- Streaming predictive state updates with `AgentState` content +- Managing shared state between client and server +- Using JSON serialization contexts for state types + +**Run the server:** + +```bash +cd Step05_StateManagement/Server dotnet run ``` -Try asking the agent to perform sensitive operations like "Send an email to user@example.com" or "Transfer $500 from account 1234 to account 5678". +The server runs on port 8888 by default. + +#### Client (`Step05_StateManagement/Client`) + +A client that displays and updates shared state from the server. Try asking: "Create a recipe for chocolate chip cookies" or "Suggest a pasta dish". + +**Run the client:** + +```bash +cd Step05_StateManagement/Client +dotnet run +``` ## How AG-UI Works @@ -236,9 +296,9 @@ For the most up-to-date AG-UI features, see the [Python samples](../../../../pyt ### Related Documentation -- [AG-UI Overview](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/) - Complete AG-UI documentation -- [Getting Started Tutorial](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/getting-started) - Step-by-step walkthrough -- [Backend Tool Rendering](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/backend-tool-rendering) - Function tools tutorial -- [Human-in-the-Loop](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/human-in-the-loop) - Approval workflows tutorial -- [State Management](https://learn.microsoft.com/azure/agent-framework/integrations/ag-ui/state-management) - State management tutorial -- [Agent Framework Overview](https://learn.microsoft.com/azure/agent-framework/overview/agent-framework-overview) - Core framework concepts +- [AG-UI Overview](https://learn.microsoft.com/agent-framework/integrations/ag-ui/) - Complete AG-UI documentation +- [Getting Started Tutorial](https://learn.microsoft.com/agent-framework/integrations/ag-ui/getting-started) - Step-by-step walkthrough +- [Backend Tool Rendering](https://learn.microsoft.com/agent-framework/integrations/ag-ui/backend-tool-rendering) - Function tools tutorial +- [Human-in-the-Loop](https://learn.microsoft.com/agent-framework/integrations/ag-ui/human-in-the-loop) - Approval workflows tutorial +- [State Management](https://learn.microsoft.com/agent-framework/integrations/ag-ui/state-management) - State management tutorial +- [Agent Framework Overview](https://learn.microsoft.com/agent-framework/overview/agent-framework-overview) - Core framework concepts From 05f93926eaf7269b78b977d098d257c1be45936a Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Mon, 17 Nov 2025 15:51:16 +0100 Subject: [PATCH 7/9] Address feedback and further cleanups --- .../Step01_GettingStarted/Client/Program.cs | 38 +++---------- .../Step01_GettingStarted/test-client.ps1 | 10 ---- .../Step02_BackendTools/Client/Program.cs | 38 +++---------- .../AGUI/Step02_BackendTools/test-client.ps1 | 10 ---- .../Step03_FrontendTools/Client/Program.cs | 38 +++---------- .../AGUI/Step03_FrontendTools/test-client.ps1 | 10 ---- .../ServerFunctionApprovalClientAgent.cs | 8 +-- .../Server/Properties/launchSettings.json | 4 +- .../Step05_StateManagement/Client/Program.cs | 55 +++++-------------- .../Step05_StateManagement/test-client.ps1 | 10 ---- .../Properties/launchSettings.json | 12 ---- 11 files changed, 46 insertions(+), 187 deletions(-) delete mode 100644 dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/test-client.ps1 delete mode 100644 dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/test-client.ps1 delete mode 100644 dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/test-client.ps1 delete mode 100644 dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/test-client.ps1 delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs index 48f1c0bfec..d942314806 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Program.cs @@ -28,41 +28,21 @@ try { - // Check if command-line argument provided - bool autoMode = args.Length > 0; - List queries = autoMode ? new List(args) : new List(); - int queryIndex = 0; - while (true) { - string? message; + // Get user input + Console.Write("\nUser (:q or quit to exit): "); + string? message = Console.ReadLine(); - if (autoMode) + if (string.IsNullOrWhiteSpace(message)) { - if (queryIndex >= queries.Count) - { - Console.WriteLine("\n[Auto-mode complete]\n"); - break; - } - message = queries[queryIndex++]; - Console.WriteLine($"\nUser: {message}"); + Console.WriteLine("Request cannot be empty."); + continue; } - else - { - // Get user input - Console.Write("\nUser (:q or quit to exit): "); - message = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(message)) - { - Console.WriteLine("Request cannot be empty."); - continue; - } - if (message is ":q" or "quit") - { - break; - } + if (message is ":q" or "quit") + { + break; } messages.Add(new ChatMessage(ChatRole.User, message)); diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/test-client.ps1 deleted file mode 100644 index d2abd889e7..0000000000 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/test-client.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$env:AGUI_SERVER_URL="http://localhost:8888" -Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" - -# Test with a simple question -$input = @" -What is 2 + 2? -quit -"@ - -$input | dotnet run diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs index 2b587ec9e6..1919a9565f 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Program.cs @@ -28,41 +28,21 @@ try { - // Check if command-line argument provided - bool autoMode = args.Length > 0; - List queries = autoMode ? new List(args) : new List(); - int queryIndex = 0; - while (true) { - string? message; + // Get user input + Console.Write("\nUser (:q or quit to exit): "); + string? message = Console.ReadLine(); - if (autoMode) + if (string.IsNullOrWhiteSpace(message)) { - if (queryIndex >= queries.Count) - { - Console.WriteLine("\n[Auto-mode complete]\n"); - break; - } - message = queries[queryIndex++]; - Console.WriteLine($"\nUser: {message}"); + Console.WriteLine("Request cannot be empty."); + continue; } - else - { - // Get user input - Console.Write("\nUser (:q or quit to exit): "); - message = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(message)) - { - Console.WriteLine("Request cannot be empty."); - continue; - } - if (message is ":q" or "quit") - { - break; - } + if (message is ":q" or "quit") + { + break; } messages.Add(new ChatMessage(ChatRole.User, message)); diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/test-client.ps1 deleted file mode 100644 index d2abd889e7..0000000000 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/test-client.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$env:AGUI_SERVER_URL="http://localhost:8888" -Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" - -# Test with a simple question -$input = @" -What is 2 + 2? -quit -"@ - -$input | dotnet run diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs index 3faf403a08..d295ed7116 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Program.cs @@ -41,41 +41,21 @@ static string GetUserLocation() try { - // Check if command-line argument provided - bool autoMode = args.Length > 0; - List queries = autoMode ? new List(args) : new List(); - int queryIndex = 0; - while (true) { - string? message; + // Get user input + Console.Write("\nUser (:q or quit to exit): "); + string? message = Console.ReadLine(); - if (autoMode) + if (string.IsNullOrWhiteSpace(message)) { - if (queryIndex >= queries.Count) - { - Console.WriteLine("\n[Auto-mode complete]\n"); - break; - } - message = queries[queryIndex++]; - Console.WriteLine($"\nUser: {message}"); + Console.WriteLine("Request cannot be empty."); + continue; } - else - { - // Get user input - Console.Write("\nUser (:q or quit to exit): "); - message = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(message)) - { - Console.WriteLine("Request cannot be empty."); - continue; - } - if (message is ":q" or "quit") - { - break; - } + if (message is ":q" or "quit") + { + break; } messages.Add(new ChatMessage(ChatRole.User, message)); diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/test-client.ps1 deleted file mode 100644 index d2abd889e7..0000000000 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/test-client.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$env:AGUI_SERVER_URL="http://localhost:8888" -Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" - -# Test with a simple question -$input = @" -What is 2 + 2? -quit -"@ - -$input | dotnet run diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs index acaa888309..41538085db 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/ServerFunctionApprovalClientAgent.cs @@ -198,17 +198,17 @@ private static AgentRunResponseUpdate ProcessIncomingServerApprovalRequests( var functionCallArgs = (Dictionary?)approvalRequest.FunctionArguments? .Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(Dictionary))); - var aprovalRequestContent = new FunctionApprovalRequestContent( + var approvalRequestContent = new FunctionApprovalRequestContent( id: approvalRequest.ApprovalId, new FunctionCallContent( callId: approvalRequest.ApprovalId, name: approvalRequest.FunctionName, arguments: functionCallArgs)); - aprovalRequestContent.AdditionalProperties ??= []; - aprovalRequestContent.AdditionalProperties["original_function"] = content; + approvalRequestContent.AdditionalProperties ??= []; + approvalRequestContent.AdditionalProperties["original_function"] = content; - updatedContents[i] = aprovalRequestContent; + updatedContents[i] = approvalRequestContent; } } diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Properties/launchSettings.json b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Properties/launchSettings.json index 3eb863e0cd..e75f8f51e3 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Properties/launchSettings.json +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Properties/launchSettings.json @@ -16,9 +16,7 @@ "launchBrowser": true, "applicationUrl": "https://localhost:7047;http://localhost:5100", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "AZURE_OPENAI_ENDPOINT": "https://ag-ui-agent-framework.openai.azure.com/", - "AZURE_OPENAI_DEPLOYMENT_NAME": "gpt-4.1-mini" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs index b905a33578..49ffa0587d 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Program.cs @@ -7,10 +7,6 @@ using Microsoft.Extensions.AI; using RecipeClient; -// Check for command-line arguments for automated testing -string[] queries = args.Length > 0 ? args : []; -int queryIndex = 0; - string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888"; Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n"); @@ -44,42 +40,25 @@ { while (true) { - string? message; + // Get user input + Console.Write("\nUser (:q to quit, :state to show state): "); + string? message = Console.ReadLine(); - if (queries.Length > 0) + if (string.IsNullOrWhiteSpace(message)) { - if (queryIndex >= queries.Length) - { - Console.WriteLine("\n[Auto-mode complete]\n"); - break; - } - message = queries[queryIndex++]; - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine($"\nQuery: {message}"); - Console.ResetColor(); + Console.WriteLine("Request cannot be empty."); + continue; } - else - { - // Get user input - Console.Write("\nUser (:q to quit, :state to show state): "); - message = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(message)) - { - Console.WriteLine("Request cannot be empty."); - continue; - } - if (message is ":q" or "quit") - { - break; - } + if (message is ":q" or "quit") + { + break; + } - if (message.Equals(":state", StringComparison.OrdinalIgnoreCase)) - { - DisplayState(agent.State.Recipe); - continue; - } + if (message.Equals(":state", StringComparison.OrdinalIgnoreCase)) + { + DisplayState(agent.State.Recipe); + continue; } messages.Add(new ChatMessage(ChatRole.User, message)); @@ -142,12 +121,6 @@ { DisplayState(agent.State.Recipe); } - - // Exit after one query in automated mode - if (queries.Length > 0 && queryIndex >= queries.Length) - { - break; - } } } catch (Exception ex) diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/test-client.ps1 b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/test-client.ps1 deleted file mode 100644 index d2abd889e7..0000000000 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/test-client.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$env:AGUI_SERVER_URL="http://localhost:8888" -Set-Location "d:\work\agent-framework-docs\Validation\AGUI\Step01_GettingStarted\Client" - -# Test with a simple question -$input = @" -What is 2 + 2? -quit -"@ - -$input | dotnet run diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json deleted file mode 100644 index ccadf94b3a..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "Microsoft.Agents.AI.Hosting.A2A.UnitTests": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:55034;http://localhost:55035" - } - } -} \ No newline at end of file From a909268b4fa35e3a6819b2a58e7872531cbbb406 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 5 Dec 2025 12:21:05 +0100 Subject: [PATCH 8/9] Fix build --- .../AGUIDojoServer/ChatClientAgentFactory.cs | 58 +++++++++---------- .../Client/Client.csproj | 2 +- .../Server/Server.csproj | 2 +- .../Step02_BackendTools/Client/Client.csproj | 2 +- .../Step02_BackendTools/Server/Server.csproj | 2 +- .../Step03_FrontendTools/Client/Client.csproj | 2 +- .../Step03_FrontendTools/Server/Server.csproj | 2 +- .../Step04_HumanInLoop/Client/Client.csproj | 2 +- .../Step04_HumanInLoop/Server/Server.csproj | 2 +- .../Client/Client.csproj | 2 +- .../Server/Server.csproj | 2 +- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs index 630bc099ce..5d6d95d554 100644 --- a/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs +++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs @@ -78,22 +78,22 @@ public static AIAgent CreateAgenticUI(JsonSerializerOptions options) { Name = "AgenticUIAgent", Description = "An agent that generates agentic user interfaces using Azure OpenAI", - 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. - """, 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, @@ -132,23 +132,23 @@ public static AIAgent CreatePredictiveStateUpdates(JsonSerializerOptions options { Name = "PredictiveStateUpdatesAgent", Description = "An agent that demonstrates predictive state updates using Azure OpenAI", - 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. - """, 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, diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj index 1b02976cad..a76a2b37ef 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj index f01019e9f4..b1e7fe33cf 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj index 1b02976cad..a76a2b37ef 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj index f01019e9f4..b1e7fe33cf 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj index 1b02976cad..a76a2b37ef 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj index f01019e9f4..b1e7fe33cf 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj index 1b02976cad..a76a2b37ef 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj index f01019e9f4..b1e7fe33cf 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj index 1b02976cad..a76a2b37ef 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj index f01019e9f4..b1e7fe33cf 100644 --- a/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj +++ b/dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable From 515881b6a03789abc95f9265d759e41a1d27556b Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 5 Dec 2025 13:46:01 +0100 Subject: [PATCH 9/9] Missing fixes --- dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj | 7 +------ dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj | 4 +--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj b/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj index a1fe8b8f34..b28e53df6e 100644 --- a/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj +++ b/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj @@ -1,17 +1,12 @@ - net9.0 + net10.0 enable enable true - - - - - diff --git a/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj b/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj index f8d134dc23..c45adfd4a8 100644 --- a/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj +++ b/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable @@ -10,8 +10,6 @@ - -