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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@
<Folder Name="/Samples/GettingStarted/DeclarativeAgents/">
<Project Path="samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/">
<File Path="samples/GettingStarted/AGUI/README.md" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step01_GettingStarted/">
<Project Path="samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step02_BackendTools/">
<Project Path="samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step03_FrontendTools/">
<Project Path="samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step04_HumanInLoop/">
<Project Path="samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AGUI/Step05_StateManagement/">
<Project Path="samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj" />
<Project Path="samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/DevUI/">
<File Path="samples/GettingStarted/DevUI/README.md" />
<Project Path="samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;
using AGUIDojoServer.AgenticUI;
using AGUIDojoServer.BackendToolRendering;
using AGUIDojoServer.PredictiveStateUpdates;
using AGUIDojoServer.SharedState;

namespace AGUIDojoServer;

[JsonSerializable(typeof(WeatherInfo))]
[JsonSerializable(typeof(Recipe))]
[JsonSerializable(typeof(Ingredient))]
[JsonSerializable(typeof(RecipeResponse))]
[JsonSerializable(typeof(Plan))]
[JsonSerializable(typeof(Step))]
[JsonSerializable(typeof(StepStatus))]
[JsonSerializable(typeof(StepStatus?))]
[JsonSerializable(typeof(JsonPatchOperation))]
[JsonSerializable(typeof(List<JsonPatchOperation>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(DocumentState))]
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft. All rights reserved.

using System.ComponentModel;

namespace AGUIDojoServer.AgenticUI;

internal static class AgenticPlanningTools
{
[Description("Create a plan with multiple steps.")]
public static Plan CreatePlan([Description("List of step descriptions to create the plan.")] List<string> steps)
{
return new Plan
{
Steps = [.. steps.Select(s => new Step { Description = s, Status = StepStatus.Pending })]
};
}

[Description("Update a step in the plan with new description or status.")]
public static async Task<List<JsonPatchOperation>> UpdatePlanStepAsync(
[Description("The index of the step to update.")] int index,
[Description("The new description for the step (optional).")] string? description = null,
[Description("The new status for the step (optional).")] StepStatus? status = null)
{
var changes = new List<JsonPatchOperation>();

if (description is not null)
{
changes.Add(new JsonPatchOperation
{
Op = "replace",
Path = $"/steps/{index}/description",
Value = description
});
}

if (status.HasValue)
{
// Status must be lowercase to match AG-UI frontend expectations: "pending" or "completed"
string statusValue = status.Value == StepStatus.Pending ? "pending" : "completed";
changes.Add(new JsonPatchOperation
{
Op = "replace",
Path = $"/steps/{index}/status",
Value = statusValue
});
}

await Task.Delay(1000);

return changes;
}
}
Original file line number Diff line number Diff line change
@@ -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<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}

public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track function calls that should trigger state events
var trackedFunctionCalls = new Dictionary<string, FunctionCallContent>();

await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Process contents: track function calls and emit state events for results
List<AIContent> stateEventsToEmit = new();
foreach (var content in update.Contents)
{
if (content is FunctionCallContent callContent)
{
if (callContent.Name == "create_plan" || callContent.Name == "update_plan_step")
{
trackedFunctionCalls[callContent.CallId] = callContent;
break;
}
}
else if (content is FunctionResultContent resultContent)
{
// Check if this result matches a tracked function call
if (trackedFunctionCalls.TryGetValue(resultContent.CallId, out var matchedCall))
{
var bytes = JsonSerializer.SerializeToUtf8Bytes((JsonElement)resultContent.Result!, this._jsonSerializerOptions);

// Determine event type based on the function name
if (matchedCall.Name == "create_plan")
{
stateEventsToEmit.Add(new DataContent(bytes, "application/json"));
}
else if (matchedCall.Name == "update_plan_step")
{
stateEventsToEmit.Add(new DataContent(bytes, "application/json-patch+json"));
}
}
}
}

yield return update;

yield return new 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
};
}
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
11 changes: 11 additions & 0 deletions dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace AGUIDojoServer.AgenticUI;

internal sealed class Plan
{
[JsonPropertyName("steps")]
public List<Step> Steps { get; set; } = [];
}
14 changes: 14 additions & 0 deletions dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace AGUIDojoServer.AgenticUI;

[JsonConverter(typeof(JsonStringEnumConverter<StepStatus>))]
internal enum StepStatus
{
Pending,
Completed
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

using System.Text.Json.Serialization;

namespace AGUIDojoServer;
namespace AGUIDojoServer.BackendToolRendering;

internal sealed class WeatherInfo
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
ChatOptions = new ChatOptions
{
Instructions = """
When planning use tools only, without any other messages.
IMPORTANT:
- Use the `create_plan` tool to set the initial state of the steps
- Use the `update_plan_step` tool to update the status of each step
- Do NOT repeat the plan or summarise it in a message
- Do NOT confirm the creation or updates in a message
- Do NOT ask the user for additional information or next steps
- Do NOT leave a plan hanging, always complete the plan via `update_plan_step` if one is ongoing.
- Continue calling update_plan_step until all steps are marked as completed.

Only one plan can be active at a time, so do not call the `create_plan` tool
again until all the steps in current plan are completed.
""",
Tools = [
AIFunctionFactory.Create(
AgenticPlanningTools.CreatePlan,
name: "create_plan",
description: "Create a plan with multiple steps.",
AGUIDojoServerSerializerContext.Default.Options),
AIFunctionFactory.Create(
AgenticPlanningTools.UpdatePlanStepAsync,
name: "update_plan_step",
description: "Update a step in the plan with new description or status.",
AGUIDojoServerSerializerContext.Default.Options)
],
AllowMultipleToolCalls = false
}
});

return new AgenticUIAgent(baseAgent, options);
}

public static AIAgent CreateSharedState(JsonSerializerOptions options)
Expand All @@ -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",
ChatOptions = new ChatOptions
{
Instructions = """
You are a document editor assistant. When asked to write or edit content:

IMPORTANT:
- Use the `write_document` tool with the full document text in Markdown format
- Format the document extensively so it's easy to read
- You can use all kinds of markdown (headings, lists, bold, etc.)
- However, do NOT use italic or strike-through formatting
- You MUST write the full document, even when changing only a few words
- When making edits to the document, try to make them minimal - do not change every word
- Keep stories SHORT!
- After you are done writing the document you MUST call a confirm_changes tool after you call write_document

After the user confirms the changes, provide a brief summary of what you wrote.
""",
Tools = [
AIFunctionFactory.Create(
WriteDocument,
name: "write_document",
description: "Write a document. Use markdown formatting to format the document.",
AGUIDojoServerSerializerContext.Default.Options)
]
}
});

return new PredictiveStateUpdatesAgent(baseAgent, options);
}

[Description("Get the weather for a given location.")]
private static WeatherInfo GetWeather([Description("The location to get the weather for.")] string location) => new()
{
Expand All @@ -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";
}
}
Loading
Loading