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:

6๊ฐœ์˜ ์ข‹์•„์š”

์ข‹์€ ์ƒ˜ํ”Œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

1๊ฐœ์˜ ์ข‹์•„์š”