Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
refactor: improve formatting and structure in EchoAgent and add MapAg…
…entEndpoint extension method
  • Loading branch information
ckpearson committed May 28, 2025
commit 9be07d8dc146a989e040edd8d7a35938d15914af
56 changes: 38 additions & 18 deletions dotnet-sdk/AGUIDotnet/Agent/EchoAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ namespace AGUIDotnet.Agent;
/// </summary>
public sealed class EchoAgent : IAGUIAgent
{
public async Task RunAsync(RunAgentInput input, ChannelWriter<BaseEvent> events, CancellationToken ct = default)
public async Task RunAsync(
RunAgentInput input,
ChannelWriter<BaseEvent> events,
CancellationToken ct = default
)
{
await events.WriteAsync(new RunStartedEvent
{
ThreadId = input.ThreadId,
RunId = input.RunId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
}, ct);
await events.WriteAsync(
new RunStartedEvent
{
ThreadId = input.ThreadId,
RunId = input.RunId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
},
ct
);

var lastMessage = input.Messages.LastOrDefault();

Expand All @@ -26,47 +33,60 @@ await events.WriteAsync(new RunStartedEvent
switch (lastMessage)
{
case SystemMessage system:
foreach (var ev in EventHelpers.SendSimpleMessage($"Echoing system message:\n\n```\n{system.Content}\n```\n"))
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing system message:\n\n```\n{system.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct);
}
break;

case UserMessage user:
foreach (var ev in EventHelpers.SendSimpleMessage($"Echoing user message:\n\n```\n{user.Content}\n```\n"))
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing user message:\n\n```\n{user.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct);
}
break;

case AssistantMessage assistant:
foreach (var ev in EventHelpers.SendSimpleMessage($"Echoing assistant message:\n\n```\n{assistant.Content}\n```\n"))
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing assistant message:\n\n```\n{assistant.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct);
}
break;

case ToolMessage tool:
foreach (var ev in EventHelpers.SendSimpleMessage($"Echoing tool message for tool call '{tool.ToolCallId}':\n\n```\n{tool.Content}\n```\n"))
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Echoing tool message for tool call '{tool.ToolCallId}':\n\n```\n{tool.Content}\n```\n"
))
{
await events.WriteAsync(ev, ct);
}
break;

default:
foreach (var ev in EventHelpers.SendSimpleMessage($"Unknown message type: {lastMessage?.GetType().Name ?? "null"}"))
foreach (var ev in EventHelpers.SendSimpleMessage(
$"Unknown message type: {lastMessage?.GetType().Name ?? "null"}"
))
{
await events.WriteAsync(ev, ct);
}
break;
}

await events.WriteAsync(new RunFinishedEvent
{
ThreadId = input.ThreadId,
RunId = input.RunId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
}, ct);
await events.WriteAsync(
new RunFinishedEvent
{
ThreadId = input.ThreadId,
RunId = input.RunId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
},
ct
);

events.Complete();
}
Expand Down
59 changes: 59 additions & 0 deletions dotnet-sdk/AGUIDotnet/Integrations/RouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using AGUIDotnet.Agent;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.AI;
using AGUIDotnet.Types;
using AGUIDotnet.Events;
using Microsoft.Extensions.Options;
using System.Text.Json;

namespace AGUIDotnet.Integrations;

public static class RouteBuilderExtensions
{
/// <summary>
/// Simple extension method to map an AGUI agent endpoint that uses server sent events to the provided route builder
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to map the POST endpoint to</param>
/// <param name="id">The ID of the agent which also becomes the mapped endpoint pattern</param>
/// <param name="agentFactory">Factory to resolve the agent instance</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/></returns>
public static IEndpointConventionBuilder MapAgentEndpoint(
this IEndpointRouteBuilder builder,
string id,
Func<IServiceProvider, IAGUIAgent> agentFactory
)
{
return builder.MapPost(
id,
async (
[FromBody] RunAgentInput input,
HttpContext context,
IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions> jsonOptions
) =>
{
context.Response.ContentType = "text/event-stream";
await context.Response.Body.FlushAsync();

var serOptions = jsonOptions.Value.SerializerOptions;
var agent = agentFactory(context.RequestServices);

await foreach (var ev in agent.RunToCompletionAsync(input, context.RequestAborted))
{
var serializedEvent = JsonSerializer.Serialize(ev, serOptions);
await context.Response.WriteAsync($"data: {serializedEvent}\n\n");
await context.Response.Body.FlushAsync();

// If the event is a RunFinishedEvent, we can break the loop.
if (ev is RunFinishedEvent)
{
break;
}
}
}
);
}
}
93 changes: 35 additions & 58 deletions dotnet-sdk/AGUIDotnetWebApiExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Extensions.AI;
using System.Collections.Immutable;
using System.ComponentModel;
using AGUIDotnet.Integrations;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -32,6 +33,18 @@
opts.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});

builder.Services.AddChatClient(
new AzureOpenAIClient(
new Uri(builder.Configuration["AzureOpenAI:Endpoint"]!),
new ApiKeyCredential(builder.Configuration["AzureOpenAI:ApiKey"]!),
new AzureOpenAIClientOptions
{
Transport = new ApiVersionSelectorTransport(builder.Configuration["AzureOpenAI:ApiVersion"]!)
}
).GetChatClient(builder.Configuration["AzureOpenAI:Model"]).AsIChatClient()
)
.UseFunctionInvocation();

var app = builder.Build();

// Configure the HTTP request pipeline.
Expand Down Expand Up @@ -131,66 +144,30 @@ You are a helpful assistant acting as a general-purpose chatbot.
var finalUsage = agent.Usage;
});

agentsGroup.MapPost("recipe", async (
[FromBody] RunAgentInput input,
HttpContext context,
IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions> jsonOptions
) =>
{
context.Response.ContentType = "text/event-stream";
await context.Response.Body.FlushAsync();

var serOpts = jsonOptions.Value.SerializerOptions;

var azureOpenAiClient = new AzureOpenAIClient(
new Uri(app.Configuration["AzureOpenAI:Endpoint"]!),
new ApiKeyCredential(app.Configuration["AzureOpenAI:ApiKey"]!),
new AzureOpenAIClientOptions
{
Transport = new ApiVersionSelectorTransport(app.Configuration["AzureOpenAI:ApiVersion"]!)
}
);

var chatClient = new ChatClientBuilder(azureOpenAiClient.GetChatClient(app.Configuration["AzureOpenAI:Model"]!).AsIChatClient())
.UseFunctionInvocation()
.Build();

var agent = new StatefulChatClientAgent<Recipe>(
chatClient,
new Recipe
{

},
new StatefulChatClientAgentOptions<Recipe>
{
PerformAiContextExtraction = false,
SystemMessage = """
<persona>
You are a helpful assistant that collaborates with the user to help create a recipe aligned with their requests and requirements.
</persona>

<rules>
- You have a tool for setting the background colour on the frontend, whenever setting a recipe, please set the background to be inspired by it.
</rules>
"""
}
);

await foreach (var ev in agent.RunToCompletionAsync(input, context.RequestAborted))
agentsGroup.MapAgentEndpoint(
"recipe",
sp =>
{
var serializedEvent = JsonSerializer.Serialize(ev, serOpts);
await context.Response.WriteAsync($"data: {serializedEvent}\n\n");
await context.Response.Body.FlushAsync();

// If the event is a RunFinishedEvent, we can break the loop.
if (ev is RunFinishedEvent)
{
break;
}
var chatClient = sp.GetRequiredService<IChatClient>();
return new StatefulChatClientAgent<Recipe>(
chatClient,
new Recipe(),
new StatefulChatClientAgentOptions<Recipe>
{
SystemMessage = """
<persona>
You are a helpful assistant that collaborates with the user to help create a recipe aligned with their requests and requirements.
</persona>

<rules>
- You have a tool for setting the background colour on the frontend, whenever setting a recipe, please set the background to be inspired by it.
</rules>
""",
PerformAiContextExtraction = false
}
);
}

var finalUsage = agent.Usage;
});
);

app.Run();

Expand Down