MS Agent Framework의 AG-UI 사용기

AI 에이전트를 만들다보면, 지금은 흩어진 여러 도구들을 모으거나, 일반 프론트엔드 애플리케이션에서 일일이 AI 에이전트가 떠들어대는 응답을 어떻게 표시할 것인지에 대해서 너무 세부적인 사항들을 고민해야하다보니 실용적이거나 재사용 가능한 코드를 만드는 느낌이 전혀 들지 않았습니다. 특히 닷넷의 경우에는 원하는 목적지까지 가는 과정에서의 ceremony나 boilerplate가 너무 많기도 했고요 :sob:

오랫만에 MS Agent Framework 문서를 보다가 AG-UI 라는 것을 찾게 되서 좀 살펴보니 그간 제가 답답하다고 느꼈던 부분들을 전부 해소해주는 느낌이라 반가운 마음에 코드 샘플을 공유해봅니다.

일단 AG-UI는 기존에 일일이 손으로 구현해야 했던 많은 부분들을 재사용 가능하게 포장해주는 도구이고, 닷넷만의 기술이 아니라 AI 에이전트 기술에 참여하는 곳이라면 너나할것없이 표준으로 제공하는 프로토콜이라 정말 사용하기 좋습니다. 즉, 랭체인으로 AG-UI 서버를 만들고, 닷넷 데스크톱 클라이언트나 블레이저 WASM 앱이 붙는 구성이 충분히 가능합니다.

그리고 AG-UI를 사용하면, (1) 서버가 제공하는 도구 호출은 물론, (2) 서버가 직접 처리할 수 없고 클라이언트에게 위임해야 하는 도구 호출까지 한 번에 구성할 수 있습니다.

AG-UI 서버 측 구현 (ASP .NET Core Kestrel)

사용하기 원하는 IChatClient 구현체와 tool calling을 지원하는 적절한 모델을 찾아 연결하기만 하면, 에이전트를 위한 시스템 프롬프트를 힘들게 고민하지 않아도 나머지는 프레임워크 레벨에서 전부 대행해줍니다.

그리고 SSE 방식의 출력을 Kestrel에 직접 통합할 수 있어서, 원하는 subpath 주소를 agent 전용으로 할당하고 클라이언트가 이 주소를 찾아서 연결할 수 있게 포장할 수 있습니다. (이 부분이 정말 좋은 부분입니다.)

#:sdk Microsoft.NET.Sdk.Web
#:package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore@1.0.0-preview.251114.1
#:package Microsoft.Extensions.AI.OpenAI@10.0.0-preview.1.25560.10

using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using OpenAI;
using System.ClientModel;
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<MathTools>();
builder.Services.AddChatClient(
	new OpenAIClient(
		new ApiKeyCredential(Util.GetPassword("openrouter-key")),
		new OpenAIClientOptions { Endpoint = new Uri("https://openrouter.ai/api/v1", UriKind.Absolute), }
	).GetChatClient("x-ai/grok-4.1-fast").AsIChatClient());

using var app = builder.Build();
var mathTool = app.Services.GetRequiredService<MathTools>();
var agent = app.Services.GetRequiredService<IChatClient>().CreateAIAgent(
	name: "AGUIAssistant",
	instructions: "You are a helpful assistant.",
	tools: [
		AIFunctionFactory.Create(mathTool.Add, nameof(mathTool.Add), "Add two numbers"),
		AIFunctionFactory.Create(mathTool.Add, nameof(mathTool.Sub), "Subtract two numbers"),
	]);
app.MapAGUI("/aguitest", agent);
app.Run();

public class MathTools(ILogger<MathTools> logger)
{
	public int Add(int a, int b)
	{
		logger.LogInformation("Add tool called: {a}, {b}", a, b);
		return a + b;
	}

	public int Sub(int a, int b)
	{
		logger.LogInformation("Subtool called: {a}, {b}", a, b);
		return a - b;
	}
}

클라이언트

클라이언트의 경우에 서버와 사전에 약속한 주소로 연결하면 됩니다. AG-UI 서버에게 “나 이런 툴 가지고 있어”라고 알리고 사용하게끔 유도하고 싶다면, tools 파라미터에 서버에서 만들었던 것과 똑같은 방법으로 도구를 등록하면, 프롬프트에 따라서 자동으로 호출 대행이 이루어지게 됩니다.

특히 이 클라이언트 방식은 아무리 레거시 기술이라 할지라도 (WinForm, WPF라 할지라도) AI 기능에 가볍게 통합된다는 점이 큰 무기입니다.

#:package Microsoft.Agents.AI.AGUI@1.0.0-preview.251114.1
#:package Microsoft.Agents.AI@1.0.0-preview.251114.1

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;

var serverUrl = "http://localhost:5000/aguitest";
Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n");

using var httpClient = new HttpClient()
{
	Timeout = TimeSpan.FromSeconds(60)
};

AGUIChatClient chatClient = new(httpClient, serverUrl);

AIAgent agent = chatClient.CreateAIAgent(
	name: "agui-client",
	description: "AG-UI Client Agent",
    tools: []);

AgentThread thread = agent.GetNewThread();
List<ChatMessage> messages =
[
	new(ChatRole.System, "You are a helpful assistant.")
];

try
{
	while (true)
	{
		// Get user input
		Console.Write("\nUser (:q or quit to exit): ");
		string? message = Console.ReadLine();

		if (string.IsNullOrWhiteSpace(message))
		{
			Console.WriteLine("Request cannot be empty.");
			continue;
		}

		if (message is ":q" or "quit")
		{
			break;
		}

		messages.Add(new 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}");
}

더 자세한 내용은 이곳에서 보실 수 있습니다. 아직은 프리뷰 버전이지만, 충분히 테스트해볼 가치가 있는 기술이라고 생각합니다. :smiley:

8개의 좋아요

좋은 샘플 감사합니다!

1개의 좋아요