Microsoft Agent Framework 1.0 i MCP w praktyce
| Semantic Kernel | AutoGen | Microsoft Agent Framework | |
|---|---|---|---|
| Rola | SDK do orkiestracji AI dla systemów enterprise | Framework badawczy do systemów multi-agentowych | Ujednolicona platforma agentowa klasy enterprise |
| Fundament | Skills, pluginy, workflow i integracje | Współpracujące agenty i konwersacyjna orkiestracja | Połączenie orkiestracji enterprise i architektury multi-agentowej |
| Interoperacyjność | Pluginy, konektory oraz wsparcie dla MCP A2A OpenAPI | Elastyczna integracja agentów i narzędzi, rozwijające się wsparcie protokołów | Wbudowane konektory, natywne wsparcie dla MCP A2A OpenAPI |
| Pamięć | Abstrakcja pamięci i integracje z bazami wektorowymi | Pamięć konwersacji oraz zewnętrzne magazyny pamięci | Trwała, adaptacyjna i wielowarstwowa pamięć agentów |
| Orkiestracja | Deterministyczna + dynamiczna (workflow, planners, process framework) | Dynamiczna orkiestracja LLM (debata, reflection, group chat, facilitator/worker) | Deterministyczna + dynamiczna orkiestracja agentów i workflow |
| Gotowość enterprise |
Telemetria, observability, compliance, integracja z Azure | Mała, stworzona do badań | Observability, approvals, CI/CD, durable execution, hydration |
| Grupa docelowa |
Zespoły enterprise budujące aplikacje AI | Research i eksperymenty z agentami AI | Zespoły budujące produkcyjne systemy agentowe |
| Ewolucja | Fundament orkiestracji enterprise AI | Fundament architektur multi-agent | Połączenie Semantic Kernel + AutoGen w jeden ekosystem |
Nie warto ślepo ufać marketingowym slajdom
W mojej opinii Semantic Kernel miał wiele problemów architektonicznych
Microsoft Agent Framework to naprawdę dojrzały framework produkcyjny
Pod koniec prezentacji pokażę ponad 10 powodów, dlaczego Microsoft Agent Framework jest lepszy
using Microsoft.Extensions.AI;
using OllamaSharp;
// Klient Ollama : domyślnie http://localhost:11434
var ollama = new OllamaApiClient("http://localhost:11434");
// IChatClient z Microsoft.Extensions.AI
IChatClient client = ollama
.AsChatClient("qwen3:8b");
var response = await client.GetResponseAsync(
"Co to jest Model Context Protocol?"
);
Console.WriteLine(response.Text);
<PackageReference Include="Microsoft.Extensions.AI"
Version="10.5.2" />
<PackageReference Include="OllamaSharp"
Version="5.*" />
using Microsoft.Extensions.AI;
using OllamaSharp;
IChatClient client = new OllamaApiClient("http://localhost:11434")
.AsChatClient("qwen3:8b");
// Historia czatu : lista wiadomości
var history = new List<ChatMessage>
{
new(ChatRole.System, "Jesteś pomocnym asystentem .NET.")
};
while (true)
{
Console.Write("\nTy: ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) break;
history.Add(new(ChatRole.User, input));
// Przekazujemy całą historię : model "pamięta" kontekst
var response = await client.GetResponseAsync(history);
Console.WriteLine($"\nModel: {response.Text}");
// Dodajemy odpowiedź modelu do historii
history.Add(response.Message);
}
await foreach (var update in
client.
GetStreamingResponseAsync(history))
{
Console.Write(update.Text);
}
Oto przykład mojego kodu, który uruchamia czat i podpina Playwright MCP do testowania strony onboardingowej TalentFlow.
Kod jest na GitHubie, link pod koniec prezentacji.
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using OpenAI;
using PlaywrightMcpDemo;
using System.ClientModel;
using System.Text;
using System.Text.Json;
const string OLLAMA_URL = "http://localhost:11434/v1";
const string OLLAMA_MODEL = "qwen3:14b";
const string DEMO_PAGE_URL = "http://localhost:5500/talentflow-onboarding.html";
const int SCENARIO_TIMEOUT_MINUTES = 20;
var scenario = (args.FirstOrDefault() ?? "A").ToUpperInvariant();
var headless = args.Contains("--headless");
var useClaude = args.Contains("--claude");
var titles = new Dictionary<string, string>
{
["A"] = "Scenariusz A - Autonomiczny Agent HR",
["B"] = "Scenariusz B - Generator Testow E2E",
["C"] = "Scenariusz C - Audytor Dostepnosci WCAG",
};
if (!titles.TryGetValue(scenario, out var title))
{
Console.WriteLine("Uzycie: dotnet run -- [A|B|C] [--headless] [--claude]");
return;
}
ConsoleUi.WriteBanner(title);
// ── Sprawdz strone demo ──────────────────────────────────────
Console.Write($"Sprawdzam {DEMO_PAGE_URL} ... ");
if (!await DemoPageChecker.IsAvailableAsync(DEMO_PAGE_URL))
{
ConsoleUi.WriteError("Strona demo niedostepna!");
Console.WriteLine("Uruchom serwer: npx serve wwwroot -l 5500");
Console.WriteLine(" lub: python -m http.server 5500 --directory wwwroot");
return;
}
ConsoleUi.WriteOk("strona dostepna");
// ── Playwright MCP ───────────────────────────────────────────
Console.WriteLine("Uruchamianie Playwright MCP Server...");
var (command, arguments) = PlaywrightProcessResolver.Resolve(headless);
Console.WriteLine($"Uruchamianie: {command} {string.Join(" ", arguments)}");
await PlaywrightProcessResolver.VerifyAsync(command, arguments);
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "playwright",
Command = command,
Arguments = arguments,
});
await using var mcp = await McpClient.CreateAsync(transport);
IList<McpClientTool> mcpTools = await mcp.ListToolsAsync();
ConsoleUi.WriteOk($"Playwright MCP gotowy ({mcpTools.Count} narzedzi)");
// ── Model LLM ────────────────────────────────────────────────
var modelName = useClaude ? "claude-haiku-4-5 (Anthropic API)" : $"{OLLAMA_MODEL} (Ollama)";
IChatClient innerClient = useClaude
? new Anthropic.AnthropicClient()
.AsIChatClient("claude-haiku-4-5")
: new OpenAIClient(
new ApiKeyCredential("ollama"),
new OpenAIClientOptions { Endpoint = new Uri(OLLAMA_URL) })
.GetChatClient(OLLAMA_MODEL)
.AsIChatClient();
IChatClient chat = new ChatClientBuilder(innerClient)
.UseFunctionInvocation()
.Build();
ConsoleUi.WriteOk($"Model: {modelName}");
Console.WriteLine();
// ── Prompty ──────────────────────────────────────────────────
var (systemPrompt, userPrompt) = await PromptLoader.LoadAsync(scenario, DEMO_PAGE_URL);
// ── Agent ────────────────────────────────────────────────────
var messages = new List<ChatMessage>
{
new(ChatRole.System, systemPrompt),
new(ChatRole.User, userPrompt),
};
var chatOptions = new ChatOptions
{
Tools = [.. mcpTools],
MaxOutputTokens = 16384,
};
Console.WriteLine("Agent startuje...");
ConsoleUi.WriteSeparator();
Console.WriteLine();
var output = new StringBuilder();
var toolsUsed = 0;
var toolCalls = new List<string>();
const int MAX_CONTINUATIONS = 5;
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(SCENARIO_TIMEOUT_MINUTES));
ConsoleUi.ResetTimer();
try
{
for (int continuation = 0; continuation <= MAX_CONTINUATIONS; continuation++)
{
if (continuation > 0)
{
ConsoleUi.WriteColored($"\n--- kontynuacja {continuation}/{MAX_CONTINUATIONS} ---", ConsoleColor.DarkYellow);
messages.Add(new(ChatRole.User, "Kontynuuj od miejsca w ktorym skonczyles. Nie powtarzaj juz wykonanych krokow."));
}
var turnOutput = new StringBuilder();
await foreach (var update in chat.GetStreamingResponseAsync(messages, chatOptions, cts.Token))
{
if (update.Text is { Length: > 0 } text)
{
Console.Write(text);
output.Append(text);
turnOutput.Append(text);
}
foreach (var call in update.Contents.OfType<FunctionCallContent>())
{
toolsUsed++;
toolCalls.Add($"{call.Name} {JsonSerializer.Serialize(call.Arguments)}");
ConsoleUi.WriteToolCall(toolsUsed, call.Name, call.Arguments);
}
foreach (var result in update.Contents.OfType<FunctionResultContent>())
{
ConsoleUi.WriteToolResult(result.Result?.ToString());
}
}
var fullText2 = output.ToString();
var done = fullText2.Contains("EMP-") ||
fullText2.Contains("Onboarding Complete") ||
fullText2.Contains("Employee ID");
if (done)
break;
if (turnOutput.Length > 0)
messages.Add(new(ChatRole.Assistant, turnOutput.ToString()));
}
}
catch (OperationCanceledException)
{
Console.WriteLine();
ConsoleUi.WriteError($"Timeout! Agent przekroczyl limit {SCENARIO_TIMEOUT_MINUTES} minut.");
}
// ── Post-processing ──────────────────────────────────────────
Console.WriteLine();
Console.WriteLine();
ConsoleUi.WriteSeparator();
var fullText = output.ToString().Trim();
var resultsDir = ResultSaver.GetResultsDir();
var activeModel = useClaude ? "claude-haiku-4-5" : OLLAMA_MODEL;
switch (scenario)
{
case "A":
await ResultSaver.SaveScenarioA(fullText, toolCalls, toolsUsed, resultsDir, activeModel);
break;
case "B":
await ResultSaver.SaveScenarioB(fullText, toolsUsed, resultsDir, activeModel);
break;
case "C":
await ResultSaver.SaveScenarioC(fullText, toolsUsed, resultsDir, activeModel);
break;
}
Console.WriteLine();
Console.WriteLine($"Akcji MCP wykonanych: {toolsUsed}");
Console.WriteLine($"Czas: {ConsoleUi.Elapsed}");
ConsoleUi.WriteColored("Gotowe!", ConsoleColor.Green);
ConsoleUi.WaitForKey();
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace PlaywrightMcpDemo;
public static class PlaywrightProcessResolver
{
public static (string Command, string[] Arguments) Resolve(bool headless)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return ("npx", BuildArgs(headless));
}
var candidates = new[]
{
Path.Combine(Environment.GetFolderPath
(Environment.SpecialFolder.ProgramFiles), "nodejs", "npx.cmd"),
Path.Combine(Environment.GetFolderPath
(Environment.SpecialFolder.ProgramFilesX86), "nodejs", "npx.cmd"),
Path.Combine(Environment.GetFolderPath
(Environment.SpecialFolder.ApplicationData), "nvm", "current", "npx.cmd"),
Path.Combine(Environment.GetFolderPath
(Environment.SpecialFolder.ApplicationData), "nvm", "v24.14.0", "npx.cmd"),
Path.Combine(Environment.GetFolderPath
(Environment.SpecialFolder.ApplicationData), "nvm", "v22.0.0", "npx.cmd"),
};
var npxCmd = candidates.FirstOrDefault(File.Exists);
if (npxCmd != null)
{
return (npxCmd, BuildArgs(headless));
}
var fallbackArgs = new List<string> { "/c", "npx" };
fallbackArgs.AddRange(BuildArgs(headless));
return ("cmd", fallbackArgs.ToArray());
}
public static async Task<bool> VerifyAsync(string command, string[] arguments)
{
try
{
var proc = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = string.Join
(" ",
arguments.Select(a =>
a.Contains(' ') ? $"\"{a}\"" : a)),
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
}
};
proc.Start();
await Task.Delay(500);
if (proc.HasExited)
{
var stderr = await proc.StandardError.ReadToEndAsync();
ConsoleUi.WriteError("Proces @playwright/mcp zakonczyl sie natychmiast!");
if (proc.ExitCode != 0)
ConsoleUi.WriteError($"Exit code: {proc.ExitCode}");
if (!string.IsNullOrWhiteSpace(stderr))
ConsoleUi.WriteError($"Stderr: {stderr}");
Console.WriteLine();
Console.WriteLine("Najczestsza przyczyna: brak przegladarek Playwright");
Console.WriteLine("Fix: npx playwright install chromium");
Console.WriteLine();
return false;
}
proc.Kill(entireProcessTree: true);
return true;
}
catch (Exception ex)
{
ConsoleUi.WriteError($"Nie mozna uruchomic npx: {ex.Message}");
return false;
}
}
private static string[] BuildArgs(bool headless)
{
return headless
? ["-y", "@playwright/mcp@latest", "--headless"]
: ["-y", "@playwright/mcp@latest"];
}
}
public static class PromptLoader
{
private const string PromptsDir = "prompts";
public static async Task<(string System, string User)>
LoadAsync(string scenario, string demoPageUrl)
{
var key = scenario.ToLowerInvariant();
var systemPath = Path.Combine(PromptsDir, $"scenario-{key}-system.md");
var userPath = Path.Combine(PromptsDir, $"scenario-{key}-user.md");
if (!File.Exists(systemPath) || !File.Exists(userPath))
throw new FileNotFoundException
($"Brak promptow dla scenariusza {scenario} w {PromptsDir}/");
var system = await File.ReadAllTextAsync(systemPath);
var user = await File.ReadAllTextAsync(userPath);
user = user.Replace("{{DEMO_PAGE_URL}}", demoPageUrl);
return (system, user);
}
}
using System.Text;
using System.Text.RegularExpressions;
namespace PlaywrightMcpDemo;
public static class ResultSaver
{
public static string GetResultsDir()
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd-HHmm");
var dir = Path.Combine("results", timestamp);
Directory.CreateDirectory(dir);
return dir;
}
public static async Task SaveScenarioA
(string fullText, List<string> toolCalls, int toolsUsed, string resultsDir, string model)
{
var allCalls = string.Join(" ", toolCalls);
var nav = toolCalls.Any(c => c.Contains("browser_navigate"));
var step1 =
(allCalls.Contains("Jan")
|| allCalls.Contains("firstName") || allCalls.Contains("e66"))
&& (allCalls.Contains("Kowalski")
|| allCalls.Contains("lastName") || allCalls.Contains("e70"))
&& (allCalls.Contains("@")
|| allCalls.Contains("email"));
var step2 = allCalls.Contains(".NET") || allCalls.Contains("position")
|| allCalls.Contains("e114") || allCalls.Contains("B2B")
|| allCalls.Contains("startDate") || allCalls.Contains("e140");
var step3 = allCalls.Contains("GitHub") || allCalls.Contains("ThinkPad")
|| allCalls.Contains("checkbox") || allCalls.Contains("Medicover")
|| allCalls.Contains("Datadog");
var step4 = allCalls.Contains("Complete") || allCalls.Contains("Submit")
|| fullText.Contains("Complete") || fullText.Contains("submit");
var empId = Regex.IsMatch(fullText + allCalls, @"EMP-\d+|NVC-\d+|employee.?id",
RegexOptions.IgnoreCase);
Console.WriteLine();
Console.WriteLine("Analiza przebiegu:");
ConsoleUi.WriteStep("Nawigacja do strony", nav);
ConsoleUi.WriteStep("Krok 1 - Personal Data", step1);
ConsoleUi.WriteStep("Krok 2 - Employment Details", step2);
ConsoleUi.WriteStep("Krok 3 - System Access", step3);
ConsoleUi.WriteStep("Krok 4 - Submit", step4);
ConsoleUi.WriteStep("Employee ID odczytany", empId);
var steps = new[] { nav, step1, step2, step3, step4, empId };
var passed = steps.Count(x => x);
Console.WriteLine();
ConsoleUi.WriteColored($"Wynik: {passed}/6 krokow zaliczonych",
passed == 6 ? ConsoleColor.Green :
passed >= 4 ? ConsoleColor.Yellow : ConsoleColor.Red);
var report = new StringBuilder();
report.AppendLine($"# Scenariusz A : Autonomiczny Agent HR");
report.AppendLine($"**Data:** {DateTime.Now:yyyy-MM-dd HH:mm}");
report.AppendLine($"**Model:** {model}");
report.AppendLine($"**Akcji MCP:** {toolsUsed}");
report.AppendLine();
report.AppendLine("## Wyniki");
report.AppendLine($"- Nawigacja: {(nav ? "✅" : "❌")}");
report.AppendLine($"- Krok 1 Personal Data: {(step1 ? "✅" : "❌")}");
report.AppendLine($"- Krok 2 Employment: {(step2 ? "✅" : "❌")}");
report.AppendLine($"- Krok 3 System Access: {(step3 ? "✅" : "❌")}");
report.AppendLine($"- Krok 4 Submit: {(step4 ? "✅" : "❌")}");
report.AppendLine($"- Employee ID: {(empId ? "✅" : "❌")}");
report.AppendLine();
report.AppendLine($"**Wynik: {passed}/6**");
report.AppendLine();
report.AppendLine("## Output agenta");
report.AppendLine("```");
report.AppendLine(fullText);
report.AppendLine("```");
report.AppendLine();
report.AppendLine("## Tool calls");
for (int i = 0; i < toolCalls.Count; i++)
report.AppendLine($"{i + 1}. {toolCalls[i]}");
var path = Path.Combine(resultsDir, "scenario-a-report.md");
await File.WriteAllTextAsync(path, report.ToString());
ConsoleUi.WriteColored($"Zapisano: {path}", ConsoleColor.Green);
}
public static async Task SaveScenarioB
(string fullText, int toolsUsed, string resultsDir, string model)
{
var match = Regex.Match(fullText, @"(import \{.*)", RegexOptions.Singleline);
var code = match.Success ? match.Value : fullText;
code = Regex.Replace(code, @"```\w*\n?|```", "").Trim();
var specPath = Path.Combine(resultsDir, "onboarding.spec.ts");
await File.WriteAllTextAsync(specPath, code);
ConsoleUi.WriteColored($"Zapisano: {specPath}", ConsoleColor.Green);
Console.WriteLine(" Uruchom: npx playwright test onboarding.spec.ts --headed");
var report = new StringBuilder();
report.AppendLine($"# Scenariusz B : Generator Testow E2E");
report.AppendLine($"**Data:** {DateTime.Now:yyyy-MM-dd HH:mm}");
report.AppendLine($"**Model:** {model}");
report.AppendLine($"**Akcji MCP:** {toolsUsed}");
report.AppendLine();
report.AppendLine("## Wygenerowany test");
report.AppendLine("```typescript");
report.AppendLine(code);
report.AppendLine("```");
var reportPath = Path.Combine(resultsDir, "scenario-b-report.md");
await File.WriteAllTextAsync(reportPath, report.ToString());
ConsoleUi.WriteColored($"Zapisano: {reportPath}", ConsoleColor.Green);
}
public static async Task SaveScenarioC
(string fullText, int toolsUsed, string resultsDir, string model)
{
var report = new StringBuilder();
report.AppendLine($"# Raport Dostepnosci WCAG 2.1 AA");
report.AppendLine($"**Aplikacja:** TalentFlow HR Onboarding Portal");
report.AppendLine($"**Data audytu:** {DateTime.Now:yyyy-MM-dd HH:mm}");
report.AppendLine($"**Audytor:** AI Agent ({model} via Playwright MCP)");
report.AppendLine($"**Standard:** WCAG 2.1 Level AA");
report.AppendLine();
report.AppendLine("---");
report.AppendLine();
report.AppendLine(fullText);
var path = Path.Combine(resultsDir, "accessibility-report.md");
await File.WriteAllTextAsync(path, report.ToString());
ConsoleUi.WriteColored($"Zapisano: {path}", ConsoleColor.Green);
Console.WriteLine();
Console.WriteLine("Znalezione problemy:");
ConsoleUi.WriteColored($" CRITICAL : {Count(fullText, "CRITICAL")}",
Count(fullText, "CRITICAL") > 0 ? ConsoleColor.Red : ConsoleColor.DarkGray);
ConsoleUi.WriteColored($" MAJOR : {Count(fullText, "MAJOR")}",
Count(fullText, "MAJOR") > 0 ? ConsoleColor.DarkYellow : ConsoleColor.DarkGray);
ConsoleUi.WriteColored($" MINOR : {Count(fullText, "MINOR")}", ConsoleColor.Yellow);
ConsoleUi.WriteColored($" OK : {Count(fullText, "OK")}", ConsoleColor.Green);
}
private static int Count(string s, string p) =>
(s.Length - s.Replace(p, "").Length) / p.Length;
}
Jestes certyfikowanym audytorem dostepnosci cyfrowej WCAG 2.1 AA.
Eksplorujesz strony przez Playwright MCP uzywajac browser_snapshot do analizy.
Dokumentujesz KAZDY problem z konkretna lokalizacja i poziomem WCAG.
Format problemow w raporcie:
CRITICAL - blokuje uzycie (WCAG A)
MAJOR - utrudnia znaczaco (WCAG AA)
MINOR - do poprawki w przyszlosci
OK - zgodne z WCAG
Przeprowadz pelny audyt WCAG 2.1 AA formularza: {{DEMO_PAGE_URL}}
Przejdz przez KAZDY krok (1 do 4) klikajac Continue i sprawdz:
PERCEIVABLE:
- Czy pola maja etykiety powiazane przez for/id lub aria-label?
- Czy ikony i przyciski bez tekstu maja aria-label?
- Czy kontrast tekstu spelnia minimum 4.5:1?
- Czy informacje nie sa przekazywane tylko kolorem?
OPERABLE:
- Czy wszystko jest dostepne klawiatura (Tab, Enter, Space)?
- Czy focus indicator jest widoczny?
- Czy checkboxy i radio buttons maja proper ARIA?
UNDERSTANDABLE:
- Czy wymagane pola sa wyraznie oznaczone?
- Czy lang jest ustawiony na elemencie html?
- Czy struktura naglowkow h1 do h3 jest logiczna?
ROBUST:
- Czy role ARIA sa poprawnie uzyte?
- Czy formularz ma poprawna semantyke HTML?
Wygeneruj raport Markdown z:
1. Podsumowaniem (liczba problemow per poziom)
2. Lista wszystkich problemow z rekomendacjami naprawy
3. Ocena koncowa w skali 0 do 100
Pisanie całego systemu agentowego od zera mija się z celem
Potrzebujemy klocków, które pozwalają budować agentów i całe procesy przepływu w systemie wieloagentowym.
Opowiem Wam, jak wyglądają one w MAF
Jakie fundamenty trzeba znać, budując system jednoagentowy i wieloagentowy?
Tak, aplikacja może pamiętać kluczowe fakty o zadaniu i o Tobie, podobnie jak Claude
Jest też wsparcie dla reducerów pamięci
Stworzyłem 4 wersje tego projektu, a Wam pokażę najlepszą
IChatClient chatClient = new Anthropic.AnthropicClient()
.AsIChatClient("claude-haiku-4-5");
//MAF jest zbudowany
//na IChatClient z Microsoft.Extensions.AI:
IChatClient chatClient =
new OpenAIClient("OPENAI_API_KEY")
.AsIChatClient("gpt-5");
IChatClient chatClient =
new AzureOpenAIClient(
new Uri("https://YOUR_RESOURCE.openai.azure.com/"),
new AzureKeyCredential("AZURE_KEY"))
.AsIChatClient("gpt-4.1");
IChatClient chatClient =
new OllamaApiClient(
new Uri("http://localhost:11434"),
"qwen3:14b")
.AsIChatClient();
IChatClient chatClient =
new GoogleAiClient("GOOGLE_API_KEY")
.AsIChatClient("gemini-2.5-pro");
IChatClient chatClient =
new MistralClient("MISTRAL_API_KEY")
.AsIChatClient("mistral-large");
IChatClient chatClient =
new OpenAIClient(
new OpenAIAuthentication("GROQ_API_KEY"),
new OpenAIClientSettings
{
Endpoint = new Uri("https://api.groq.com/openai/v1")
})
.AsIChatClient("llama-3.3-70b");
IChatClient chatClient =
new OpenAIClient(
new OpenAIAuthentication("OPENROUTER_KEY"),
new OpenAIClientSettings
{
Endpoint = new Uri("https://openrouter.ai/api/v1")
})
.AsIChatClient("anthropic/claude-sonnet-4");
IChatClient chatClient =
new OpenAIClient(
new OpenAIAuthentication("lm-studio"),
new OpenAIClientSettings
{
Endpoint = new Uri("http://localhost:1234/v1")
})
.AsIChatClient("local-model");
IChatClient chatClient =
new OpenAIClient(
new OpenAIAuthentication("NVIDIA_API_KEY"),
new OpenAIClientSettings
{
Endpoint = new Uri("https://integrate.api.nvidia.com/v1")
})
.AsIChatClient("meta/llama-3.1-70b-instruct");
IChatClient chatClient =
new OpenAIClient(
new OpenAIAuthentication("localai"),
new OpenAIClientSettings
{
Endpoint = new Uri("http://localhost:8080/v1")
})
.AsIChatClient("local-model");
Można podmienić providera (OpenAI, Azure, Ollama...) bez zmiany kodu agentów.
Są gotowe middleware dla IChatClient (rate limiting, logging, retry), które działają transparentnie
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Agents.AI;
using OpenAI;
// ==========================================
// 1. Niski poziom -> IChatClient
// ==========================================
IChatClient chatClient =
new OpenAIClient("OPENAI_API_KEY")
.AsIChatClient("gpt-4.1");
var response = await chatClient.GetResponseAsync(
"Napisz krótki post o .NET 10");
Console.WriteLine(response.Text);
//prompt -> model -> response
// ==========================================
// 2. Wyższa abstrakcja -> Agent
// ==========================================
ChatClientAgent linkedinAgent = new(
chatClient,
new ChatClientAgentOptions
{
Name = "LinkedInWriter",
Instructions = """
Jesteś seniorem copywriterem LinkedIn.
Zasady:
- pisz technicznie
- używaj krótkich akapitów
- max 2 emoji
- dodaj CTA
- odpowiadaj po polsku
"""
});
var agentResponse = await linkedinAgent.RunAsync(
"Napisz post o .NET 10");
await foreach (var message in agentResponse)
{
Console.Write(message.Text);
}
//user
//↓
//Agent
//↓
//Instructions
//Tools
//Memory
//Context
//Routing
//Policies
//↓
//IChatClient
//↓
//LLM
RequestPort jest executorem w grafie workflow.
Workflow nie może go ominąć
Jest częścią grafu, nie opcjonalnym narzędziem agenta.
// W event loop klienta:
if (evt is RequestInfoEvent ri)
{
// Wyświetl podsumowanie użytkownikowi
// Pobierz decyzję (t/n + feedback)
var decision = new ApprovalDecision(approved, feedback);
await run.SendResponseAsync(
evt.Request.CreateResponse(decision));
}
Skills to paczki wiedzy domenowej w formacie SKILL.md
Markdown z instrukcjami, które agent ładuje na żądanie (progressive disclosure).
Nie upychamy wszystkiego w system prompt.
var skillsProvider = new AgentSkillsProvider(
Path.Combine(AppContext.BaseDirectory, "skills"));
// Podpięcie do agenta jako context provider:
_agent = new ChatClientAgent
(chatClient, new ChatClientAgentOptions
{
Name = "Copywriter",
AIContextProviders = [skillsProvider],
ChatOptions = new() { Instructions = "..." }
});
AgentSkillsProvider skanuje katalog skills/, odkrywa pliki SKILL.md i rejestruje je
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<YouTubeTranscriptTool>();
var app = builder.Build();
app.MapMcp("/mcp");
app.Run("http://localhost:2001");
[McpServerToolType]
public class YouTubeTranscriptTool
{
[McpServerTool(Name = "get_youtube_transcript")]
[Description("Pobiera transkrypcję wideo z YouTube.")]
public static string Execute(
[Description("URL wideo YouTube")] string videoUrl)
{
Console.WriteLine($"[MCP Server] Pobieram: {videoUrl}");
return """
Tytuł: "Microsoft Agent Framework 1.0 : Production Ready!"
Kanał: dotNET (Microsoft), Data: 7 maja 2026
Kluczowe punkty:
- MAF 1.0 = fuzja Semantic Kernel + AutoGen → stabilne API, SLA produkcyjne
- Workflows: sequential, concurrent, handoff : wszystko ze streamingiem
- MCP jako pierwszoklasowy obywatel: dynamiczne odkrywanie tools
- Human-in-the-Loop: ApprovalRequiredAIFunction : jeden wrapper
- Agent Skills: wiedza dziedzinowa w SKILL.md, progressive disclosure
""";
}
}
// Połączenie z serwerem MCP (HTTP transport):
await using var mcpClient = await McpClient.CreateAsync(
new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost:2001/mcp")
}));
// Pobranie listy narzędzi:
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();
// Przekazanie narzędzi do agenta:
_agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
ChatOptions = new()
{
Instructions = "...",
Tools = [.. mcpTools] // spread operator
}
});
ChatOptions = new()
{
Tools = [AIFunctionFactory.Create(GetLinkedInTrends)]
}
[Description("Sprawdza popularne trendy na LinkedIn w kategorii tech/AI")]
static string GetLinkedInTrends(
[Description("Kategoria, np. 'AI', '.NET'")] string category)
{
return $"Top trendy w kategorii \"{category}\" (symulacja): ...";
}
//Kluczowa różnica vs Semantic Kernel
//W SK trzeba było: stworzyć plugin
// → zarejestrować go w Kernel
// → oznaczyć [KernelFunction].
//W MAF wystarczy zwykła metoda C# + AIFunctionFactory.Create()
// : zero boilerplate'u, zero zależności od obiektu Kernel.
| Executor | Typ | Co robi |
|---|---|---|
ResearcherExecutor |
AI Agent + narzędzia MCP | Pobiera transkrypcję YT, wyodrębnia tezy |
CopywriterExecutor |
AI Agent + Skills | Pisze draft posta na LinkedIn |
StrategExecutor |
AI Agent + Skills + C# function tool | Analizuje trendy, proponuje strategię |
AggregatorExecutor |
Deterministyczny | Zbiera wyniki fan-in (draft + strategia) |
RedaktorExecutor |
AI Agent | Scala draft i strategię w finalny post |
PublishExecutor |
Deterministyczny | Symuluje publikację, robi YieldOutput |
PrepareApprovalExecutor<T> |
Deterministyczny | Przygotowuje podsumowanie do zatwierdzenia |
ApprovalGateExecutor<T> |
Deterministyczny | Sprawdza decyzję, audit log, przepuszcza/odrzuca |
// Executor z typowanym wejściem i wyjściem:
public partial class RedaktorExecutor :
Executor<EditorialInput, FinalPost>
// Executor z wejściem bez typowanego wyjścia (wysyła ręcznie):
public partial class AggregatorExecutor() :
Executor<object>("Aggregator")
// Executor z wejściem i YieldOutput:
public partial class PublishExecutor() :
Executor<FinalPost>("Publish")
Executor zapewnia separację: każdy krok jest niezależny, testowalny, może być AI lub zwykłym kodem.
Executor NIE jest agentem AI.
Executor to warstwa orkiestracyjna, która używa agenta. Agent jest wstrzykiwany z zewnątrz
// Executor przyjmuje gotowego AIAgent : nie tworzy go sam
public ResearcherExecutor(AIAgent agent) : base("Researcher")
{
_agent = agent;
}
Workflow defininiowany jako graf executorów z krawędziami definiującymi przepływ danych i zależności
var workflow = new WorkflowBuilder(researcher)
.AddEdge(researcher, research.Prepare)
.AddEdge(research.Prepare, research.Port)
// ...
.AddFanOutEdge(research.Gate, [copywriter, strateg])
.AddFanInBarrierEdge(fanInSources, aggregator)
// ...
.Build();
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
EventAction action = evt switch
{
ResearchCompletedEvent e =>
renderer.HandleResearchCompleted(e),
RequestInfoEvent ri =>
await renderer.HandleHITL(ri, run),
ExecutorFailedEvent e =>
renderer.HandleError(e),
WorkflowOutputEvent e =>
renderer.HandleOutput(e),
_ => EventAction.Next
};
}
public sealed class ResearchCompletedEvent
(ResearchResult result) : WorkflowEvent(result)
{
public ResearchResult Result => result;
}
await context.AddEventAsync
(new ResearchCompletedEvent(result), cancellationToken);
Każdy executor deklaruje typy wejściowe i wyjściowe za pomocą generyków i atrybutów:
[YieldsOutput(typeof(ResearchResult))]
// "ten executor produkuje ResearchResult"
public partial class ResearcherExecutor ...
[YieldsOutput(typeof(DraftPost))]
// "ten executor produkuje DraftPost"
public partial class CopywriterExecutor ...
[YieldsOutput(typeof(FinalPost))]
// "ten executor produkuje FinalPost"
public partial class RedaktorExecutor ...
[SendsMessage(typeof(EditorialInput))]
// "ten executor wysyła EditorialInput dalej"
public partial class AggregatorExecutor ...
[SendsMessage(typeof(object))]
// "ten executor wysyła dowolny typ dalej"
public partial class ApprovalGateExecutor<TData> ...
ResearchRequest
→ Researcher → ResearchResult
→ PrepareResearch → ApprovalRequest
→ RequestPort → ApprovalDecision
→ GateResearch → ResearchResult (przepuszczone)
├→ Copywriter → DraftPost
└→ Strateg → StrategyAnalysis
→ Aggregator → EditorialInput
→ PrepareEditorial → ApprovalRequest
→ RequestPort → ApprovalDecision
→ GateEditorial → EditorialInput (przepuszczone)
→ Redaktor → FinalPost
→ PreparePublish → ApprovalRequest
→ RequestPort → ApprovalDecision
→ GatePublish → FinalPost
(przepuszczone)
→ Publish → string (output)
MAF automatycznie generuje kod połączeń między elementami workflow już podczas kompilacji.
To mechanizm, w którym kompilator analizuje atrybuty na executorach i automatycznie tworzy dodatkowy kod C#, który zapewnia type-safety przepływu danych w grafie workflow.
Każde połączenie między elementami jest sprawdzane przez kompilator. Żadna wiadomość nie "zgubi się" ani nie trafi do niewłaściwego executora.
Analogia: To jak generyczne interfejsy w ASP.NET Core (IRequestHandler<TRequest, TResponse> z MediatR), ale zamiast ręcznej rejestracji w DI, source generator robi to automatycznie na podstawie atrybutów i hierarchii klas.
MAF pozwala wpiąć middleware na 3 różnych poziomach, każdy z inną sygnaturą, innym punktem interwencji i innym celem.
To kluczowa różnica vs Semantic Kernel, który miał płaski model filtrów.
┌─────────────────────────────────────────────────────────────────────┐
│ Poziom 1: Agent Run Middleware │
│ widzi: cały prompt → cała odpowiedź │
│ przykłady: Logging, Retry, Guardrails, TokenTracking │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Poziom 2: Function Calling Middleware │ │
│ │ widzi: nazwa narzędzia, argumenty → wynik narzędzia │ │
│ │ przykłady: AuditFunction, InputValidation │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Poziom 3: IChatClient Middleware │ │ │
│ │ │ widzi: surowe tokeny → surowa odpowiedź LLM │ │ │
│ │ │ przykłady: RateLimiter, CachingClient, │ │ │
│ │ │ TelemetryClient, TokenCounter │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────────────────────────┐ │ │ │
│ │ │ │ Model LLM (Anthropic, OpenAI, │ │ │ │
│ │ │ │ Ollama...) │ │ │ │
│ │ │ └──────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
public enum GuardrailAction { Block, Modify, Warn }
Block : output zablokowany, zwrócony komunikat błędu
Modify : output zmodyfikowany (np. zredagowane PII)
Warn : ostrzeżenie w logach, output przepuszczony
// ============================================================================
// Program.cs tworzy agentów, podpina middleware/guardrails, przekazuje do executorów.
//
// Pipeline middleware per agent (od zewnątrz do wewnątrz):
// Logging → Retry → Guardrails → FunctionAudit → FunctionValidation → Agent
//
// Konfiguracja per agent:
// Researcher : Logging + Retry + FunctionAudit + FunctionValidation (bez guardrails)
// Copywriter : Logging + Retry + ContentPolicy + BrandSafety + OutputLength
// Strateg : Logging + Retry + OutputLength
// Redaktor : Logging + Retry + ContentPolicy + BrandSafety + OutputLength
//
// ============================================================================
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using AiContentTeam;
using AiContentTeam.Middleware;
using AiContentTeam.Guardrails;
// ── Chat client (surowy, bez middleware na IChatClient) ────────────────────────
IChatClient chatClient = new Anthropic.AnthropicClient()
.AsIChatClient("claude-haiku-4-5");
// ── MCP tools ────────────────────────────────────────────────────────────────
await using var mcpClient = await McpClient.CreateAsync(
new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost:2001/mcp")
}));
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();
// ── Skills provider ───────────────────────────────────────────────────────────
#pragma warning disable MAAI001
var skillsProvider = new AgentSkillsProvider(
Path.Combine(AppContext.BaseDirectory, "skills"));
#pragma warning restore MAAI001
// ── Guardrails (reużywalne instancje) ─────────────────────────────────────────
var contentPolicy = new ContentPolicyGuardrail();
var brandSafety = new BrandSafetyGuardrail(
competitors: ["CompetitorA", "CompetitorB"],
requireCTA: true);
var outputLength = new OutputLengthGuardrail(maxChars: 3000);
// ══════════════════════════════════════════════════════════════════════════════
// TWORZENIE AGENTÓW Z MIDDLEWARE
// ══════════════════════════════════════════════════════════════════════════════
// ── Researcher: Logging + Retry + FunctionAudit + FunctionValidation ──────────
// Bez guardrails : pracuje na surowych danych z zewnątrz (transkrypcja YouTube).
AIAgent researcherAgent = GuardrailFactory.WrapWithFullPipeline(
new ChatClientAgent(
chatClient,
new ChatClientAgentOptions
{
Name = "Researcher",
ChatOptions = new()
{
Instructions = """
Jesteś research agentem. Twoim JEDYNYM zadaniem jest:
1. UŻYJ NARZĘDZIA GetYouTubeTranscript aby pobrać transkrypcję z podanego URL
2. Na podstawie wyniku wyodrębnij dokładnie 5 kluczowych tez jako bullet pointy
ZASADY:
- ZAWSZE wywołuj narzędzie GetYouTubeTranscript : NIGDY nie oceniaj URL samodzielnie
- Odpowiadaj po polsku
- Każdą tezę pisz w osobnej linii zaczynającej się od "• "
""",
Tools = [.. mcpTools]
}
}),
enableLogging: true,
enableRetry: true);
// Brak guardrails : celowo
// ── Copywriter: pełny pipeline z guardrails ───────────────────────────────────
AIAgent copywriterAgent = GuardrailFactory.WrapWithFullPipeline(
new ChatClientAgent(
chatClient,
new ChatClientAgentOptions
{
Name = "Copywriter",
AIContextProviders = [skillsProvider],
ChatOptions = new()
{
Instructions = """
Jesteś copywriterem specjalizującym się w treściach dla .NET developerów.
Na podstawie tez od Researchera napisz post na LinkedIn.
ZASADY:
- 150-250 słów, max 3 emoji
- Mocny hook w 1. zdaniu (zaskoczenie lub kontrowersyjny claim)
- Zakończ pytaniem do społeczności
- Napisz TYLKO treść posta, bez meta-komentarza
- Odpowiadaj po polsku
"""
}
}),
enableLogging: true,
enableRetry: true,
contentPolicy, brandSafety, outputLength);
// ── Strateg: Logging + Retry + OutputLength ───────────────────────────────────
AIAgent strategAgent = GuardrailFactory.WrapWithFullPipeline(
new ChatClientAgent(
chatClient,
new ChatClientAgentOptions
{
Name = "Strateg",
AIContextProviders = [skillsProvider],
ChatOptions = new()
{
Instructions = """
Jesteś strategiem social media dla społeczności .NET developerów.
Przeanalizuj temat i odpowiedz STRUKTURALNIE:
ANALIZA TRENDÓW: [2-3 zdania o aktualności tematu]
REKOMENDOWANY HOOK: [jedno zdanie otwierające : konkretna propozycja]
SUGEROWANE HASHTAGI: [lista, max 5, format #tag]
UWAGI O ZAANGAŻOWANIU: [1-2 zdania]
Odpowiadaj po polsku.
""",
Tools = [AIFunctionFactory.Create(GetLinkedInTrends)]
}
}),
enableLogging: true,
enableRetry: true,
outputLength);
// ── Redaktor: pełny pipeline z guardrails ─────────────────────────────────────
AIAgent redaktorAgent = GuardrailFactory.WrapWithFullPipeline(
new ChatClientAgent(
chatClient,
new ChatClientAgentOptions
{
Name = "Redaktor",
ChatOptions = new()
{
Instructions = """
Jesteś redaktorem naczelnym. Dostałeś:
1. Draft posta od Copywritera
2. Analizę strategiczną od Stratega
Weź draft Copywritera jako bazę, zastosuj rekomendacje Stratega
(hook, hashtagi, uwagi o zaangażowaniu), popraw błędy językowe.
Napisz TYLKO finalną treść posta. Bez meta-komentarzy,
bez opisywania co robisz. Sam tekst posta po polsku.
"""
}
}),
enableLogging: true,
enableRetry: true,
contentPolicy, brandSafety, outputLength);
// ══════════════════════════════════════════════════════════════════════════════
// EXECUTORY (przyjmują gotowych agentów)
// ══════════════════════════════════════════════════════════════════════════════
var researcher = new ResearcherExecutor(researcherAgent);
var copywriter = new CopywriterExecutor(copywriterAgent);
var strateg = new StrategExecutor(strategAgent);
var aggregator = new AggregatorExecutor();
var redaktor = new RedaktorExecutor(redaktorAgent);
var publisher = new PublishExecutor();
// ── HITL stages ───────────────────────────────────────────────────────────────
var research = ApprovalStage.Create<ResearchResult>(
"Research",
r => $"""
URL: {r.YouTubeUrl}
Grupa docelowa: {r.TargetAudience}
Tezy ({r.KeyPoints.Length}):
{string.Join("\n", r.KeyPoints.Select(p => $" • {p}"))}
""",
timeout: TimeSpan.FromMinutes(5));
var editorial = ApprovalStage.Create<EditorialInput>(
"Editorial",
ei => $"""
DRAFT ({ei.Draft.Content.Length} znaków):
{ei.Draft.Content}
STRATEGIA:
Hook: {ei.Strategy.RecommendedHook}
Hashtagi: {string.Join(" ", ei.Strategy.SuggestedHashtags)}
Uwagi: {ei.Strategy.EngagementNotes}
""",
timeout: TimeSpan.FromMinutes(5));
var publish = ApprovalStage.Create<FinalPost>(
"Publish",
fp => $"""
FINALNY POST (zredagowany):
{fp.Content}
Hashtagi: {string.Join(" ", fp.Hashtags)}
Zredagowany o: {fp.EditedAt}
""",
timeout: TimeSpan.FromMinutes(5));
// ── Workflow ──────────────────────────────────────────────────────────────────
IEnumerable<ExecutorBinding> fanInSources = [copywriter, strateg];
var workflow = new WorkflowBuilder(researcher)
.AddEdge(researcher, research.Prepare)
.AddEdge(research.Prepare, research.Port)
.AddEdge(research.Port, research.Gate)
.AddFanOutEdge(research.Gate, [copywriter, strateg])
.AddFanInBarrierEdge(fanInSources, aggregator)
.AddEdge(aggregator, editorial.Prepare)
.AddEdge(editorial.Prepare, editorial.Port)
.AddEdge(editorial.Port, editorial.Gate)
.AddEdge(editorial.Gate, redaktor)
.AddEdge(redaktor, publish.Prepare)
.AddEdge(publish.Prepare, publish.Port)
.AddEdge(publish.Port, publish.Gate)
.AddEdge(publish.Gate, publisher)
.WithOutputFrom(publisher)
.Build();
await CreateDiagrams(workflow);
Console.ReadKey();
// ── Uruchomienie ──────────────────────────────────────────────────────────────
ConsoleWorkflowRenderer.PrintBanner();
var input = new ResearchRequest(
YouTubeUrl: "https://youtube.com/watch?v=maf100demo",
TargetAudience: "polscy .NET developerzy, seniorzy i architekci");
var renderer = new ConsoleWorkflowRenderer(
finalAgentName: "Publish",
parallelAgents: ["Copywriter", "Strateg"]);
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
EventAction action = evt switch
{
ResearchCompletedEvent e => renderer.HandleResearchCompleted(e),
DraftReadyEvent e => renderer.HandleDraftReady(e),
StrategyReadyEvent e => renderer.HandleStrategyReady(e),
AgentProgressEvent e => renderer.HandleAgentProgress(e),
ApprovalCompletedEvent e => renderer.HandleApprovalCompleted(e),
ExecutorFailedEvent e => renderer.HandleError(e),
AgentResponseUpdateEvent e => renderer.HandleToken(e),
WorkflowOutputEvent e => renderer.HandleOutput(e),
ExecutorCompletedEvent e => renderer.HandleCompleted(e),
_ => EventAction.Next
};
if (evt is RequestInfoEvent ri)
{
action = await renderer.HandleHITL(ri, run);
}
switch (action)
{
case EventAction.Break: goto done;
case EventAction.Continue: continue;
}
}
done:
renderer.PrintSummary();
MiddlewareMetrics.PrintReport();
Console.ReadLine();
// ── Helper: GetLinkedInTrends (tool Stratega) ─────────────────────────────────
[System.ComponentModel.Description("Sprawdza popularne trendy na LinkedIn w kategorii tech/AI")]
static string GetLinkedInTrends(
[System.ComponentModel.Description("Kategoria, np. 'AI', '.NET'")] string category)
{
return $"""
Top trendy w kategorii "{category}" (symulacja):
1. Microsoft Agent Framework : wysoka aktywność
2. .NET 10 Preview : umiarkowane zaangażowanie
3. Multi-agent orchestration : rosnący trend
""";
}
// ── Diagram export ────────────────────────────────────────────────────────────
static async Task CreateDiagrams(Workflow workflow)
{
var vizDir = Path.Combine(AppContext.BaseDirectory, "workflow_viz");
Directory.CreateDirectory(vizDir);
var mermaidPath = Path.Combine(vizDir, "workflow_graph.mmd");
await File.WriteAllTextAsync(mermaidPath, workflow.ToMermaidString());
Console.WriteLine($"[VIZ] Mermaid zapisany: {mermaidPath}");
var dotPath = Path.Combine(vizDir, "workflow_graph.dot");
await File.WriteAllTextAsync(dotPath, workflow.ToDotString());
Console.WriteLine($"[VIZ] DOT zapisany: {dotPath}");
Console.WriteLine($"[VIZ] Wszystkie pliki w: {vizDir}");
Console.WriteLine();
}
public enum EventAction
{
Next,
Continue,
Break
}
// ============================================================================
// 1. Agent Run Middleware
// - LoggingMiddleware : logowanie wejścia/wyjścia + timing
// - RetryMiddleware : retry z exponential backoff
//
// 2. Function Calling Middleware
// - AuditFunctionMiddleware : audyt każdego wywołania narzędzia
// - InputValidationMiddleware : walidacja/obcinanie wyników narzędzi
//
// 3. IChatClient Middleware
// - TokenTrackingMiddleware : zlicza tokeny i koszty
//
// ============================================================================
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace AiContentTeam.Middleware;
// ══════════════════════════════════════════════════════════════════════════════
// 1. AGENT RUN MIDDLEWARE
// ══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Centralne logowanie i pomiar czasu każdego uruchomienia agenta.
/// </summary>
public static class LoggingMiddleware
{
public static async Task<AgentResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
{
var agentName = innerAgent.Name ?? "Unknown";
var messageCount = messages.Count();
var sw = Stopwatch.StartNew();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [MW:Log] ▶ {agentName} start ({messageCount} wiadomości)");
Console.ResetColor();
try
{
var response = await innerAgent.RunAsync(
messages, session, options, cancellationToken).ConfigureAwait(false);
sw.Stop();
var outputLength = response.Text?.Length ?? 0;
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [MW:Log] ◀ {agentName} zakończony " +
$"({sw.ElapsedMilliseconds}ms, {outputLength} znaków output)");
Console.ResetColor();
MiddlewareMetrics.RecordAgentRun(agentName, sw.Elapsed, success: true);
return response;
}
catch (Exception ex)
{
sw.Stop();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($" [MW:Log] ✗ {agentName} BŁĄD po {sw.ElapsedMilliseconds}ms: {ex.Message}");
Console.ResetColor();
MiddlewareMetrics.RecordAgentRun(agentName, sw.Elapsed, success: false);
throw;
}
}
public static async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var agentName = innerAgent.Name ?? "Unknown";
var sw = Stopwatch.StartNew();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [MW:Log] ▶ {agentName} streaming start");
Console.ResetColor();
await foreach (var update in innerAgent.RunStreamingAsync(
messages, session, options, cancellationToken))
{
yield return update;
}
sw.Stop();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [MW:Log] ◀ {agentName} streaming zakończony ({sw.ElapsedMilliseconds}ms)");
Console.ResetColor();
MiddlewareMetrics.RecordAgentRun(agentName, sw.Elapsed, success: true);
}
}
/// <summary>
/// Retry z exponential backoff na transient failures.
/// </summary>
public static class RetryMiddleware
{
public static int MaxRetries { get; set; } = 3;
public static TimeSpan InitialDelay { get; set; } = TimeSpan.FromSeconds(1);
public static async Task<AgentResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
{
var agentName = innerAgent.Name ?? "Unknown";
Exception? lastException = null;
for (int attempt = 0; attempt <= MaxRetries; attempt++)
{
try
{
if (attempt > 0)
{
var delay = InitialDelay * Math.Pow(2, attempt - 1);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [MW:Retry] {agentName} próba {attempt + 1}/{MaxRetries + 1} " +
$"(po {delay.TotalSeconds:F1}s)");
Console.ResetColor();
await Task.Delay(delay, cancellationToken);
}
return await innerAgent.RunAsync(
messages, session, options, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (IsTransient(ex) && attempt < MaxRetries)
{
lastException = ex;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [MW:Retry] {agentName} transient failure: {ex.Message}");
Console.ResetColor();
}
}
throw new InvalidOperationException(
$"Agent '{agentName}' failed after {MaxRetries + 1} attempts.", lastException);
}
private static bool IsTransient(Exception ex)
{
if (ex is HttpRequestException or TimeoutException
or TaskCanceledException { InnerException: TimeoutException })
{
return true;
}
if (ex is InvalidOperationException ioe
&& ioe.Message.Contains("rate limit", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
}
// ══════════════════════════════════════════════════════════════════════════════
// 2. FUNCTION CALLING MIDDLEWARE
// ══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Audytuje każde wywołanie narzędzia : kto, co, kiedy, ile trwało.
/// </summary>
public static class AuditFunctionMiddleware
{
public static async ValueTask<object?> InvokeAsync(
AIAgent agent,
FunctionInvocationContext context,
Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,
CancellationToken cancellationToken)
{
var functionName = context.Function.Name;
var agentName = agent.Name ?? "Unknown";
var sw = Stopwatch.StartNew();
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine($" [MW:Audit] 🔧 {agentName} → {functionName}()");
Console.ResetColor();
var result = await next(context, cancellationToken);
sw.Stop();
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine($" [MW:Audit] ✓ {functionName} zakończony ({sw.ElapsedMilliseconds}ms)");
Console.ResetColor();
MiddlewareMetrics.RecordFunctionCall(agentName, functionName, sw.Elapsed);
return result;
}
}
/// <summary>
/// Obcina nadmiernie długie wyniki narzędzi : zapobiega przepełnieniu kontekstu.
/// </summary>
public static class InputValidationMiddleware
{
public static int MaxResultLength { get; set; } = 10_000;
public static async ValueTask<object?> InvokeAsync(
AIAgent agent,
FunctionInvocationContext context,
Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,
CancellationToken cancellationToken)
{
var functionName = context.Function.Name;
var result = await next(context, cancellationToken);
var resultStr = result?.ToString() ?? "";
if (resultStr.Length > MaxResultLength)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [MW:Validation] ⚠ {functionName} wynik obcięty " +
$"({resultStr.Length} → {MaxResultLength} znaków)");
Console.ResetColor();
return resultStr[..MaxResultLength] + "... [TRUNCATED BY MIDDLEWARE]";
}
return result;
}
}
// ══════════════════════════════════════════════════════════════════════════════
// 3. IChatClient MIDDLEWARE
// ══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Śledzenie zużycia tokenów i szacowanie kosztów.
/// Rejestrowane WEWNĄTRZ agenta (przez clientFactory w konstruktorze executora).
/// </summary>
public static class TokenTrackingMiddleware
{
public static async Task<AgentResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
{
var response = await innerAgent.RunAsync(
messages, session, options, cancellationToken).ConfigureAwait(false);
var inputChars = messages.Sum(m => m.Text?.Length ?? 0);
var outputChars = response.Text?.Length ?? 0;
// Przybliżone szacowanie (1 token ≈ 4 znaki)
MiddlewareMetrics.RecordTokenUsage(inputChars / 4, outputChars / 4);
return response;
}
}
// ══════════════════════════════════════════════════════════════════════════════
// MIDDLEWARE METRICS
// ══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Thread-safe centralny zbieracz metryk z pipeline'u middleware.
/// </summary>
public static class MiddlewareMetrics
{
private static readonly ConcurrentBag<AgentRunMetric> _agentRuns = [];
private static readonly ConcurrentBag<FunctionCallMetric> _functionCalls = [];
private static readonly ConcurrentBag<string> _guardrailBlocks = [];
private static long _totalInputTokens;
private static long _totalOutputTokens;
public record AgentRunMetric(string AgentName, TimeSpan Duration, bool Success, DateTime Timestamp);
public record FunctionCallMetric(string AgentName, string FunctionName, TimeSpan Duration, DateTime Timestamp);
public static void RecordAgentRun(string agentName, TimeSpan duration, bool success)
=> _agentRuns.Add(new(agentName, duration, success, DateTime.UtcNow));
public static void RecordFunctionCall(string agentName, string functionName, TimeSpan duration)
=> _functionCalls.Add(new(agentName, functionName, duration, DateTime.UtcNow));
public static void RecordGuardrailBlock(string reason)
=> _guardrailBlocks.Add(reason);
public static void RecordTokenUsage(int inputTokens, int outputTokens)
{
Interlocked.Add(ref _totalInputTokens, inputTokens);
Interlocked.Add(ref _totalOutputTokens, outputTokens);
}
public static void Clear()
{
_agentRuns.Clear();
_functionCalls.Clear();
_guardrailBlocks.Clear();
Interlocked.Exchange(ref _totalInputTokens, 0);
Interlocked.Exchange(ref _totalOutputTokens, 0);
}
public static void PrintReport()
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine("\n┌─────────────────────────────────────────────────────────┐");
Console.WriteLine("│ 📊 MIDDLEWARE METRICS : Raport pipeline'u │");
Console.WriteLine("├─────────────────────────────────────────────────────────┤");
Console.WriteLine("│ Agent Runs: │");
foreach (var run in _agentRuns.OrderBy(r => r.Timestamp))
{
var icon = run.Success ? "✅" : "❌";
Console.WriteLine($"│ {icon} {run.AgentName,-18} {run.Duration.TotalMilliseconds,7:F0}ms │");
}
if (!_functionCalls.IsEmpty)
{
Console.WriteLine("│ │");
Console.WriteLine("│ Function Calls: │");
foreach (var call in _functionCalls.OrderBy(c => c.Timestamp))
{
Console.WriteLine($"│ 🔧 {call.AgentName}/{call.FunctionName,-16} " +
$"{call.Duration.TotalMilliseconds,5:F0}ms │");
}
}
if (!_guardrailBlocks.IsEmpty)
{
Console.WriteLine("│ │");
Console.WriteLine("│ Guardrail Blocks: │");
var grouped = _guardrailBlocks.GroupBy(b => b).Select(g => (g.Key, g.Count()));
foreach (var (reason, count) in grouped)
{
Console.WriteLine($"│ 🛡️ {reason,-20} ×{count} │");
}
}
Console.WriteLine("│ │");
Console.WriteLine($"│ Tokeny (szacunkowo): │");
Console.WriteLine($"│ Input: ~{Interlocked.Read(ref _totalInputTokens),8} tokenów │");
Console.WriteLine($"│ Output: ~{Interlocked.Read(ref _totalOutputTokens),8} tokenów │");
Console.WriteLine($"│ Razem: ~{Interlocked.Read(ref _totalInputTokens) + Interlocked.Read(ref _totalOutputTokens),8} tokenów │");
Console.WriteLine("└─────────────────────────────────────────────────────────┘");
Console.ResetColor();
}
}
// ============================================================================
//
// 1. ContentPolicyGuardrail : redaktuje dane wrażliwe z output (PII)
// 2. BrandSafetyGuardrail : wzmianki o konkurencji, CTA check
// 3. OutputLengthGuardrail : obcina nadmiernie długi output
//
// GuardrailFactory : composable wrapping agentów z pełnym pipeline'em
//
// ============================================================================
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.Text.RegularExpressions;
using AiContentTeam.Middleware;
namespace AiContentTeam.Guardrails;
// ══════════════════════════════════════════════════════════════════════════════
// GUARDRAIL ABSTRAKCJA
// ══════════════════════════════════════════════════════════════════════════════
public abstract class GuardrailBase
{
public string Name { get; }
protected GuardrailBase(string name) => Name = name;
public async Task<AgentResponse> ExecuteAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
{
// ── Pre-execution check ──
var preResult = await CheckInputAsync(messages, innerAgent, cancellationToken);
if (preResult is not null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($" [Guardrail:{Name}] 🛑 INPUT ZABLOKOWANY: {preResult.Reason}");
Console.ResetColor();
MiddlewareMetrics.RecordGuardrailBlock($"{Name}:pre:{preResult.Reason}");
return new AgentResponse([
new ChatMessage(ChatRole.Assistant,
$"⚠️ Guardrail '{Name}': {preResult.Reason}")
]);
}
// ── Agent execution ──
var response = await innerAgent.RunAsync(
messages, session, options, cancellationToken).ConfigureAwait(false);
// ── Post-execution check ──
var postResult = await CheckOutputAsync(response, innerAgent, cancellationToken);
return postResult switch
{
{ Action: GuardrailAction.Block } => HandleBlock(postResult),
{ Action: GuardrailAction.Modify } => HandleModify(response, postResult),
{ Action: GuardrailAction.Warn } => HandleWarn(response, postResult),
_ => response
};
}
protected virtual Task<GuardrailViolation?> CheckInputAsync(
IEnumerable<ChatMessage> messages, AIAgent agent, CancellationToken ct)
=> Task.FromResult<GuardrailViolation?>(null);
protected virtual Task<GuardrailViolation?> CheckOutputAsync(
AgentResponse response, AIAgent agent, CancellationToken ct)
=> Task.FromResult<GuardrailViolation?>(null);
private AgentResponse HandleBlock(GuardrailViolation violation)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($" [Guardrail:{Name}] 🛑 OUTPUT ZABLOKOWANY: {violation.Reason}");
Console.ResetColor();
MiddlewareMetrics.RecordGuardrailBlock($"{Name}:post:{violation.Reason}");
return new AgentResponse([
new ChatMessage(ChatRole.Assistant,
$"⚠️ Guardrail '{Name}': Output zablokowany : {violation.Reason}")
]);
}
private AgentResponse HandleModify(AgentResponse original, GuardrailViolation violation)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [Guardrail:{Name}] ✏️ OUTPUT ZMODYFIKOWANY: {violation.Reason}");
Console.ResetColor();
MiddlewareMetrics.RecordGuardrailBlock($"{Name}:modify:{violation.Reason}");
if (violation.ModifiedText is not null)
return new AgentResponse([new ChatMessage(ChatRole.Assistant, violation.ModifiedText)]);
return original;
}
private AgentResponse HandleWarn(AgentResponse original, GuardrailViolation violation)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($" [Guardrail:{Name}] ⚠️ OSTRZEŻENIE: {violation.Reason}");
Console.ResetColor();
return original;
}
}
public enum GuardrailAction { Block, Modify, Warn }
public sealed record GuardrailViolation(
string Reason,
GuardrailAction Action = GuardrailAction.Block,
string? ModifiedText = null);
// ══════════════════════════════════════════════════════════════════════════════
// GUARDRAIL: Content Policy
// ══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Post-execution: redaktuje dane wrażliwe z output (telefony, emaile).
/// Ostrzega o nadmiarze emoji.
/// </summary>
public sealed class ContentPolicyGuardrail : GuardrailBase
{
public ContentPolicyGuardrail() : base("ContentPolicy") { }
protected override Task<GuardrailViolation?> CheckOutputAsync(
AgentResponse response, AIAgent agent, CancellationToken ct)
{
var text = response.Text ?? "";
// Redakcja numerów telefonów
if (Regex.IsMatch(text, @"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b"))
return Task.FromResult<GuardrailViolation?>(
new("Output zawiera numer telefonu", GuardrailAction.Modify,
Regex.Replace(text, @"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", "[PHONE REDACTED]")));
// Redakcja emaili
if (Regex.IsMatch(text, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"))
return Task.FromResult<GuardrailViolation?>(
new("Output zawiera adres email", GuardrailAction.Modify,
Regex.Replace(text, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
"[EMAIL REDACTED]")));
// Ostrzeżenie o nadmiarze emoji
var emojiCount = Regex.Matches(text, @"[\uD83D][\uDE00-\uDE4F\uDE80-\uDEFF]|[\uD83E][\uDD00-\uDDFF]").Count;
if (emojiCount > 5)
return Task.FromResult<GuardrailViolation?>(
new($"Zbyt wiele emoji ({emojiCount}), max 5", GuardrailAction.Warn));
return Task.FromResult<GuardrailViolation?>(null);
}
}
// ══════════════════════════════════════════════════════════════════════════════
// GUARDRAIL: Brand Safety
// ══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Sprawdza wzmianki o konkurencji i obecność CTA w poście.
/// </summary>
public sealed class BrandSafetyGuardrail : GuardrailBase
{
private readonly HashSet<string> _competitors;
private readonly bool _requireCTA;
public BrandSafetyGuardrail(
IEnumerable<string>? competitors = null,
bool requireCTA = false)
: base("BrandSafety")
{
_competitors = new HashSet<string>(
competitors ?? [], StringComparer.OrdinalIgnoreCase);
_requireCTA = requireCTA;
}
protected override Task<GuardrailViolation?> CheckOutputAsync(
AgentResponse response, AIAgent agent, CancellationToken ct)
{
var text = response.Text ?? "";
foreach (var competitor in _competitors)
{
if (text.Contains(competitor, StringComparison.OrdinalIgnoreCase))
return Task.FromResult<GuardrailViolation?>(
new($"Wzmianka o konkurencji: '{competitor}'", GuardrailAction.Warn));
}
if (_requireCTA && !text.Contains('?'))
return Task.FromResult<GuardrailViolation?>(
new("Post nie zawiera pytania/CTA", GuardrailAction.Warn));
return Task.FromResult<GuardrailViolation?>(null);
}
}
// ══════════════════════════════════════════════════════════════════════════════
// GUARDRAIL: Output Length
// ══════════════════════════════════════════════════════════════════════════════
public sealed class OutputLengthGuardrail : GuardrailBase
{
private readonly int _maxChars;
public OutputLengthGuardrail(int maxChars = 3000) : base("OutputLength")
=> _maxChars = maxChars;
protected override Task<GuardrailViolation?> CheckOutputAsync(
AgentResponse response, AIAgent agent, CancellationToken ct)
{
var text = response.Text ?? "";
if (text.Length > _maxChars)
{
return Task.FromResult<GuardrailViolation?>(
new($"Output za długi ({text.Length}/{_maxChars})",
GuardrailAction.Modify,
text[.._maxChars] + $"\n\n[... obcięto z {text.Length} do {_maxChars} znaków]"));
}
return Task.FromResult<GuardrailViolation?>(null);
}
}
// ══════════════════════════════════════════════════════════════════════════════
// FACTORY
// ══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Factory do composable'owego opakowywania agentów middleware + guardrails.
///
/// Kolejność warstw (od zewnątrz):
/// Logging → Retry → Guardrails → FunctionAudit → FunctionValidation → Agent
/// </summary>
public static class GuardrailFactory
{
/// <summary>
/// Pełny pipeline: Logging + Retry + Function middleware + Guardrails.
/// </summary>
public static AIAgent WrapWithFullPipeline(
AIAgent agent,
bool enableLogging = true,
bool enableRetry = true,
params GuardrailBase[] guardrails)
{
var builder = agent.AsBuilder();
// Warstwa 1 (najbardziej zewnętrzna): Logging
if (enableLogging)
{
builder.Use(
runFunc: LoggingMiddleware.RunAsync,
runStreamingFunc: LoggingMiddleware.RunStreamingAsync);
}
// Warstwa 2: Retry
if (enableRetry)
{
builder.Use(
runFunc: RetryMiddleware.RunAsync,
runStreamingFunc: null);
}
// Warstwa 3: Token tracking
builder.Use(
runFunc: TokenTrackingMiddleware.RunAsync,
runStreamingFunc: null);
// Warstwa 4: Guardrails (każdy jako osobna warstwa)
foreach (var guardrail in guardrails)
{
builder.Use(
runFunc: guardrail.ExecuteAsync,
runStreamingFunc: null);
}
// Warstwa 4 (najbliżej agenta): Function calling middleware
builder.Use(AuditFunctionMiddleware.InvokeAsync);
builder.Use(InputValidationMiddleware.InvokeAsync);
return builder.Build();
}
}
// ============================================================================
// Przyjmuje AIAgent z zewnątrz : middleware/guardrails podpinane w Program.cs
// ============================================================================
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace AiContentTeam;
// ── Typy danych ──────────────────────────────────────────────────────────────
public sealed record ResearchRequest(
string YouTubeUrl,
string TargetAudience);
public sealed record ResearchResult(
string YouTubeUrl,
string[] KeyPoints,
string TargetAudience);
// ── Custom event ─────────────────────────────────────────────────────────────
public sealed class ResearchCompletedEvent(ResearchResult result) : WorkflowEvent(result)
{
public ResearchResult Result => result;
public override string ToString() =>
$"[Research] Wyodrębniono {result.KeyPoints.Length} tez z: {result.YouTubeUrl}";
}
// ── Executor ─────────────────────────────────────────────────────────────────
[YieldsOutput(typeof(ResearchResult))]
public partial class ResearcherExecutor : Executor<ResearchRequest, ResearchResult>
{
private readonly AIAgent _agent;
private AgentSession? _session;
/// <summary>
/// Przyjmuje gotowego AIAgent : middleware i guardrails podpięte w Program.cs.
/// </summary>
public ResearcherExecutor(AIAgent agent) : base("Researcher")
{
_agent = agent;
}
public override async ValueTask<ResearchResult> HandleAsync(
ResearchRequest message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
await context.AddEventAsync(
new AgentProgressEvent("Researcher", $"Pobieram transkrypcję z: {message.YouTubeUrl}"),
cancellationToken);
_session ??= await _agent.CreateSessionAsync(cancellationToken);
var prompt = $"""
Pobierz transkrypcję i wyodrębnij tezy z: {message.YouTubeUrl}
Grupa docelowa: {message.TargetAudience}
""";
var agentResult = await _agent.RunAsync(prompt, _session, cancellationToken: cancellationToken);
var keyPoints = agentResult.Text
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Where(line => line.TrimStart().StartsWith("• "))
.Select(line => line.TrimStart().TrimStart('•').Trim())
.ToArray();
var result = new ResearchResult(
YouTubeUrl: message.YouTubeUrl,
KeyPoints: keyPoints,
TargetAudience: message.TargetAudience);
await context.AddEventAsync(new ResearchCompletedEvent(result), cancellationToken);
return result;
}
}
// ============================================================================
//
// Wzorzec z oficjalnej dokumentacji MAF (Expense Reimbursement blog):
// PrepareExecutor → RequestPort → GateExecutor → next
//
// RequestPort JEST executorem w grafie (wewnętrznie tworzy RequestInfoExecutor).
// Nie można mieć innego executora z tym samym ID.
// Dlatego dzielimy na:
//
// 1. PrepareApprovalExecutor<TData> : konwertuje TData → ApprovalRequest
// (edge: prepare → port : port emituje RequestInfoEvent i blokuje)
//
// 2. ApprovalGateExecutor<TData> : odbiera ApprovalDecision z portu,
// loguje audit, sprawdza timeout, przepuszcza lub przerywa
// (edge: port → gate → next)
//
// Features:
// A. Timeout + auto-reject
// B. Audit log (każda decyzja logowana, walidacja post-mortem)
// C. Reusable factory: ApprovalStage.Create<T>(...)
// ============================================================================
using Microsoft.Agents.AI.Workflows;
using System.Collections.Concurrent;
namespace AiContentTeam;
// ── Typy zatwierdzeń ──────────────────────────────────────────────────────────
/// <summary>
/// Żądanie zatwierdzenia wysyłane przez RequestPort do człowieka.
/// </summary>
public sealed record ApprovalRequest(
string PhaseName,
string Summary,
DateTime RequestedAt)
{
public ApprovalRequest(string phaseName, string summary)
: this(phaseName, summary, DateTime.UtcNow) { }
}
/// <summary>
/// Odpowiedź od człowieka: approved/rejected + opcjonalny feedback.
/// </summary>
public sealed record ApprovalDecision(
bool Approved,
string? Feedback = null);
// ── Audit log ─────────────────────────────────────────────────────────────────
public sealed record ApprovalAuditEntry(
string PhaseName,
DateTime RequestedAt,
DateTime DecidedAt,
bool Approved,
string? Feedback,
bool TimedOut);
/// <summary>
/// Globalny, thread-safe audit log zatwierdzeń.
/// Waliduje że KAŻDA faza HITL się wykonała.
/// </summary>
public static class ApprovalAuditLog
{
private static readonly ConcurrentBag<ApprovalAuditEntry> _entries = [];
public static void Record(ApprovalAuditEntry entry) => _entries.Add(entry);
public static IReadOnlyList<ApprovalAuditEntry> GetAll() => [.. _entries];
public static bool AllPhasesApproved(params string[] requiredPhases)
{
var approved = new HashSet<string>(
_entries.Where(e => e.Approved).Select(e => e.PhaseName));
return requiredPhases.All(approved.Contains);
}
public static void Clear() => _entries.Clear();
public static void PrintReport()
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine("\n┌─────────────────────────────────────────────────────────┐");
Console.WriteLine("│ 📋 AUDIT LOG : Historia zatwierdzeń HITL │");
Console.WriteLine("├─────────────────────────────────────────────────────────┤");
foreach (var entry in _entries.OrderBy(e => e.RequestedAt))
{
var icon = entry.Approved ? "✅" : "❌";
var timeout = entry.TimedOut ? " (TIMEOUT)" : "";
var duration = (entry.DecidedAt - entry.RequestedAt).TotalMilliseconds;
Console.WriteLine($"│ {icon} {entry.PhaseName,-20} " +
$"{duration,6:F0}ms {timeout}");
if (!string.IsNullOrEmpty(entry.Feedback))
Console.WriteLine($"│ 💬 {entry.Feedback}");
}
Console.WriteLine("└─────────────────────────────────────────────────────────┘");
Console.ResetColor();
}
}
// ── Custom event ──────────────────────────────────────────────────────────────
public sealed class ApprovalCompletedEvent(string phaseName, bool approved, string? feedback)
: WorkflowEvent($"{phaseName}: {(approved ? "approved" : "rejected")}")
{
public string PhaseName => phaseName;
public bool Approved => approved;
public string? Feedback => feedback;
}
// ── PrepareApprovalExecutor<TData> ────────────────────────────────────────────
/// <summary>
/// Konwertuje TData na ApprovalRequest.
/// Edge do RequestPort sprawia, że port emituje RequestInfoEvent i blokuje workflow.
///
/// Graf: previousExecutor → PrepareApproval → RequestPort (blokuje!)
/// </summary>
[YieldsOutput(typeof(ApprovalRequest))]
public partial class PrepareApprovalExecutor<TData> : Executor<TData, ApprovalRequest> where TData : class
{
private readonly string _phaseName;
private readonly Func<TData, string> _summarizer;
/// <summary>Dane czekające na zatwierdzenie : GateExecutor je odczytuje.</summary>
internal TData? PendingData { get; private set; }
internal DateTime RequestedAt { get; private set; }
public PrepareApprovalExecutor(string phaseName, Func<TData, string> summarizer)
: base($"Prepare{phaseName}")
{
_phaseName = phaseName;
_summarizer = summarizer;
}
public override async ValueTask<ApprovalRequest> HandleAsync(
TData message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
PendingData = message;
RequestedAt = DateTime.UtcNow;
var summary = _summarizer(message);
await context.AddEventAsync(
new AgentProgressEvent($"Prepare{_phaseName}",
$"⏸️ Czekam na zatwierdzenie fazy: {_phaseName}"),
cancellationToken);
return new ApprovalRequest(_phaseName, summary);
}
}
// ── ApprovalGateExecutor<TData> ───────────────────────────────────────────────
/// <summary>
/// Odbiera ApprovalDecision z RequestPort.
/// Sprawdza timeout, loguje audit, przepuszcza TData dalej lub przerywa.
///
/// Graf: RequestPort → GateExecutor → nextExecutor
/// </summary>
[SendsMessage(typeof(object))]
public partial class ApprovalGateExecutor<TData> : Executor<ApprovalDecision> where TData : class
{
private readonly string _phaseName;
private readonly PrepareApprovalExecutor<TData> _prepare;
private readonly TimeSpan _timeout;
public ApprovalGateExecutor(
string phaseName,
PrepareApprovalExecutor<TData> prepare,
TimeSpan? timeout = null)
: base($"Gate{phaseName}")
{
_phaseName = phaseName;
_prepare = prepare;
_timeout = timeout ?? TimeSpan.FromMinutes(5);
}
public override async ValueTask HandleAsync(
ApprovalDecision decision,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
var decidedAt = DateTime.UtcNow;
var timedOut = (decidedAt - _prepare.RequestedAt) > _timeout;
var finalDecision = timedOut
? new ApprovalDecision(false, $"Auto-reject: timeout po {_timeout.TotalSeconds}s")
: decision;
// ── Audit log ─────────────────────────────────────────────────
ApprovalAuditLog.Record(new ApprovalAuditEntry(
PhaseName: _phaseName,
RequestedAt: _prepare.RequestedAt,
DecidedAt: decidedAt,
Approved: finalDecision.Approved,
Feedback: finalDecision.Feedback,
TimedOut: timedOut));
await context.AddEventAsync(
new ApprovalCompletedEvent(_phaseName, finalDecision.Approved, finalDecision.Feedback),
cancellationToken);
if (finalDecision.Approved && _prepare.PendingData is not null)
{
await context.SendMessageAsync(_prepare.PendingData, cancellationToken: cancellationToken);
}
else
{
var reason = finalDecision.Feedback ?? "Brak powodu";
await context.YieldOutputAsync(
$"❌ Faza '{_phaseName}' odrzucona: {reason}",
cancellationToken);
}
}
}
// ── Factory ───────────────────────────────────────────────────────────────────
/// <summary>
/// Tworzy trójkę (Prepare, Port, Gate) do wstawienia w workflow graph.
///
/// Użycie:
/// var research = ApprovalStage.Create<ResearchResult>(
/// "Research", r => $"Tezy: {r.KeyPoints.Length}");
///
/// builder
/// .AddEdge(researcher, research.Prepare)
/// .AddEdge(research.Prepare, research.Port)
/// .AddEdge(research.Port, research.Gate)
/// .AddEdge(research.Gate, nextExecutor)
/// </summary>
public static class ApprovalStage
{
public static (
PrepareApprovalExecutor<TData> Prepare,
ExecutorBinding Port,
ApprovalGateExecutor<TData> Gate)
Create<TData>(
string phaseName,
Func<TData, string> summarizer,
TimeSpan? timeout = null) where TData : class
{
var prepare = new PrepareApprovalExecutor<TData>(phaseName, summarizer);
var port = RequestPort.Create<ApprovalRequest, ApprovalDecision>($"{phaseName}Port");
var gate = new ApprovalGateExecutor<TData>(phaseName, prepare, timeout);
return (prepare, port, gate);
}
}
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace AiContentTeam;
// ── Typy danych ──────────────────────────────────────────────────────────────
public sealed record DraftPost(
string AgentName,
string Content);
public sealed record StrategyAnalysis(
string RecommendedHook,
string[] SuggestedHashtags,
string EngagementNotes);
// ── Custom eventy ─────────────────────────────────────────────────────────────
public sealed class DraftReadyEvent(DraftPost draft) : WorkflowEvent(draft)
{
public DraftPost Result => draft;
public override string ToString() =>
$"[Copywriter] Draft gotowy ({draft.Content.Length} znaków)";
}
public sealed class StrategyReadyEvent(StrategyAnalysis analysis) : WorkflowEvent(analysis)
{
public override string ToString() =>
$"[Strateg] Analiza gotowa : hook: \"{analysis.RecommendedHook}\"";
}
public sealed class AgentProgressEvent(string agentName, string status) : WorkflowEvent($"{agentName}: {status}")
{
public string AgentName => agentName;
public string Status => status;
public override string ToString() => $"[{agentName}] {status}";
}
// ── Shared State ──────────────────────────────────────────────────────────────
internal static class ResearchStateScope
{
public const string Name = "ResearchState";
public const string KeyPointsKey = "key_points";
public const string TargetAudienceKey = "target_audience";
}
// ── CopywriterExecutor ────────────────────────────────────────────────────────
[YieldsOutput(typeof(DraftPost))]
public partial class CopywriterExecutor : Executor<ResearchResult, DraftPost>
{
private readonly AIAgent _agent;
private AgentSession? _session;
/// <summary>
/// Przyjmuje gotowego AIAgent : middleware i guardrails podpięte w Program.cs.
/// </summary>
public CopywriterExecutor(AIAgent agent) : base("Copywriter")
{
_agent = agent;
}
public override async ValueTask<DraftPost> HandleAsync(
ResearchResult message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
await context.QueueStateUpdateAsync(
ResearchStateScope.KeyPointsKey,
string.Join("\n", message.KeyPoints.Select(p => $"• {p}")),
scopeName: ResearchStateScope.Name,
cancellationToken);
await context.AddEventAsync(
new AgentProgressEvent("Copywriter", "Generuję draft posta na LinkedIn..."),
cancellationToken);
_session ??= await _agent.CreateSessionAsync(cancellationToken);
var prompt = $"""
Tezy z researchu (dla grupy: {message.TargetAudience}):
{string.Join("\n", message.KeyPoints.Select(p => $"• {p}"))}
Napisz post na LinkedIn.
""";
var agentResult = await _agent.RunAsync(prompt, _session, cancellationToken: cancellationToken);
var draft = new DraftPost("Copywriter", agentResult.Text);
await context.AddEventAsync(new DraftReadyEvent(draft), cancellationToken);
return draft;
}
}
// ── StrategExecutor ───────────────────────────────────────────────────────────
[YieldsOutput(typeof(StrategyAnalysis))]
public partial class StrategExecutor : Executor<ResearchResult, StrategyAnalysis>
{
private readonly AIAgent _agent;
private AgentSession? _session;
/// <summary>
/// Przyjmuje gotowego AIAgent : middleware i guardrails podpięte w Program.cs.
/// </summary>
public StrategExecutor(AIAgent agent) : base("Strateg")
{
_agent = agent;
}
public override async ValueTask<StrategyAnalysis> HandleAsync(
ResearchResult message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
await context.AddEventAsync(
new AgentProgressEvent("Strateg", "Analizuję trendy i przygotowuję strategię..."),
cancellationToken);
_session ??= await _agent.CreateSessionAsync(cancellationToken);
var prompt = $"""
Temat: wyodrębnione tezy z materiału wideo dla grupy "{message.TargetAudience}":
{string.Join("\n", message.KeyPoints.Select(p => $"• {p}"))}
Sprawdź trendy i podaj analizę strategiczną.
""";
var agentResult = await _agent.RunAsync(prompt, _session, cancellationToken: cancellationToken);
var lines = agentResult.Text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var hook = lines.FirstOrDefault(l => l.StartsWith("REKOMENDOWANY HOOK:"))
?.Replace("REKOMENDOWANY HOOK:", "").Trim()
?? lines.FirstOrDefault() ?? "Odkryj nowe możliwości";
var hashtags = lines.FirstOrDefault(l => l.StartsWith("SUGEROWANE HASHTAGI:"))
?.Replace("SUGEROWANE HASHTAGI:", "").Trim()
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Where(h => h.StartsWith("#"))
.ToArray()
?? ["#dotnet", "#ai"];
var notes = lines.FirstOrDefault(l => l.StartsWith("UWAGI O ZAANGAŻOWANIU:"))
?.Replace("UWAGI O ZAANGAŻOWANIU:", "").Trim()
?? "Brak dodatkowych uwag.";
var analysis = new StrategyAnalysis(hook, hashtags, notes);
await context.AddEventAsync(new StrategyReadyEvent(analysis), cancellationToken);
return analysis;
}
}
// ============================================================================
// RedaktorExecutor przyjmuje AIAgent z zewnątrz
// ============================================================================
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace AiContentTeam;
// ── Typy ──────────────────────────────────────────────────────────────────────
public sealed record EditorialInput(
DraftPost Draft,
StrategyAnalysis Strategy);
public sealed record FinalPost(
string Content,
string[] Hashtags,
string EditedAt);
// ── Custom event ──────────────────────────────────────────────────────────────
public sealed class PostPublishedEvent(string content, string publishedAt) : WorkflowEvent(content)
{
public string PublishedAt => publishedAt;
public override string ToString() => $"[Publish] Post opublikowany o {publishedAt}";
}
// ── AggregatorExecutor ────────────────────────────────────────────────────────
[SendsMessage(typeof(EditorialInput))]
public partial class AggregatorExecutor() : Executor<object>("Aggregator")
{
private DraftPost? _draft;
private StrategyAnalysis? _strategy;
public override async ValueTask HandleAsync(
object message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
if (message is DraftPost d) _draft = d;
if (message is StrategyAnalysis s) _strategy = s;
if (_draft is not null && _strategy is not null)
await context.SendMessageAsync(
new EditorialInput(_draft, _strategy),
cancellationToken: cancellationToken);
}
}
// ── RedaktorExecutor ──────────────────────────────────────────────────────────
[YieldsOutput(typeof(FinalPost))]
public partial class RedaktorExecutor : Executor<EditorialInput, FinalPost>
{
private readonly AIAgent _agent;
private AgentSession? _session;
/// <summary>
/// Przyjmuje gotowego AIAgent : middleware i guardrails podpięte w Program.cs.
/// </summary>
public RedaktorExecutor(AIAgent agent) : base("Redaktor")
{
_agent = agent;
}
public override async ValueTask<FinalPost> HandleAsync(
EditorialInput message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
await context.AddEventAsync(
new AgentProgressEvent("Redaktor", "Scala draft i strategię, przygotowuje finalny post..."),
cancellationToken);
_session ??= await _agent.CreateSessionAsync(cancellationToken);
var prompt = $"""
DRAFT POSTA (od Copywritera):
{message.Draft.Content}
ANALIZA STRATEGICZNA (od Stratega):
Rekomendowany hook: {message.Strategy.RecommendedHook}
Sugerowane hashtagi: {string.Join(" ", message.Strategy.SuggestedHashtags)}
Uwagi: {message.Strategy.EngagementNotes}
Popraw draft zgodnie z analizą. Napisz TYLKO finalny tekst posta.
""";
var agentResult = await _agent.RunAsync(prompt, _session, cancellationToken: cancellationToken);
return new FinalPost(
Content: agentResult.Text,
Hashtags: message.Strategy.SuggestedHashtags,
EditedAt: DateTime.Now.ToString("HH:mm:ss"));
}
}
// ── PublishExecutor ───────────────────────────────────────────────────────────
[YieldsOutput(typeof(string))]
public partial class PublishExecutor() : Executor<FinalPost>("Publish")
{
public override async ValueTask HandleAsync(
FinalPost message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\n✅ POST OPUBLIKOWANY NA LINKEDIN:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(message.Content);
Console.WriteLine($"\n{string.Join(" ", message.Hashtags)}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Console.ResetColor();
var publishedAt = DateTime.Now.ToString("HH:mm:ss");
await context.AddEventAsync(
new PostPublishedEvent(message.Content, publishedAt),
cancellationToken);
await context.YieldOutputAsync(
$"Opublikowano o {publishedAt}",
cancellationToken);
}
}
// ============================================================================
// Wszystkie 3 HITL via RequestPort : zero zależności od agenta
// ============================================================================
using AiContentTeam;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using System.Diagnostics;
sealed class ConsoleWorkflowRenderer
{
private readonly string _finalAgentName;
private readonly HashSet<string> _parallelAgents;
private readonly HashSet<string> _seenAgents = new();
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private readonly Stopwatch _phaseStopwatch = new();
private bool _workflowOutputReceived;
private bool _parallelBannerShown;
public ConsoleWorkflowRenderer(string finalAgentName, HashSet<string> parallelAgents)
{
_finalAgentName = finalAgentName;
_parallelAgents = parallelAgents;
}
// ── Style agentów ─────────────────────────────────────────────────────────
private static readonly Dictionary<string, (ConsoleColor Color, string Icon, string Prefix)> AgentStyles = new()
{
["Researcher"] = (ConsoleColor.Cyan, "🔍", "→"),
["Copywriter"] = (ConsoleColor.Yellow, "✍️ ", "║ PARALLEL ║"),
["Strateg"] = (ConsoleColor.Blue, "📊", "║ PARALLEL ║"),
["Redaktor"] = (ConsoleColor.Magenta, "📝", "→"),
["Publish"] = (ConsoleColor.Green, "🚀", "→"),
["PrepareResearch"] = (ConsoleColor.DarkYellow, "🛑", "→ HITL"),
["PrepareEditorial"] = (ConsoleColor.DarkYellow, "🛑", "→ HITL"),
["PreparePublish"] = (ConsoleColor.DarkYellow, "🛑", "→ HITL"),
};
private static (ConsoleColor Color, string Icon, string Prefix) GetStyle(string agentName)
=> AgentStyles.GetValueOrDefault(agentName, (ConsoleColor.White, "🤖", "→"));
private static string ShortName(string executorId)
=> executorId.Split('_')[0];
// ── Progress handler ──────────────────────────────────────────────────────
public EventAction HandleAgentProgress(AgentProgressEvent evt)
{
var (color, icon, _) = GetStyle(evt.AgentName);
Console.ForegroundColor = color;
Console.WriteLine($" ⏳ {icon} {evt.AgentName}: {evt.Status}");
Console.ResetColor();
return EventAction.Next;
}
// ── Approval completed ────────────────────────────────────────────────────
public EventAction HandleApprovalCompleted(ApprovalCompletedEvent evt)
{
Console.ForegroundColor = evt.Approved ? ConsoleColor.Green : ConsoleColor.Red;
var icon = evt.Approved ? "✅" : "❌";
Console.WriteLine($"\n {icon} HITL {evt.PhaseName}: {(evt.Approved ? "ZATWIERDZONO" : "ODRZUCONO")}");
if (!string.IsNullOrEmpty(evt.Feedback))
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" 💬 Feedback: {evt.Feedback}");
}
Console.ResetColor();
return EventAction.Next;
}
// ── Custom event handlers ────────────────────────────────────────────────
public EventAction HandleResearchCompleted(ResearchCompletedEvent evt)
{
_phaseStopwatch.Stop();
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("\n┌─────────────────────────────────────────────────────────────┐");
Console.WriteLine($"│ 🔍 RESEARCH ZAKOŃCZONY : {evt.Result.KeyPoints.Length} tez wyodrębnionych");
Console.WriteLine("└─────────────────────────────────────────────────────────────┘");
Console.ResetColor();
foreach (var point in evt.Result.KeyPoints)
{
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine($" • {point}");
}
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" ⏱️ Faza 1 (Research): {_phaseStopwatch.ElapsedMilliseconds}ms");
Console.ResetColor();
_phaseStopwatch.Restart();
return EventAction.Next;
}
public EventAction HandleDraftReady(DraftReadyEvent evt)
{
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.Yellow;
var length = evt.Result.Content.Length;
Console.WriteLine($"\n ✓ Draft gotowy ({length} znaków)");
var preview = evt.Result.Content.Length > 200
? evt.Result.Content[..200] + "..."
: evt.Result.Content;
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($" ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
Console.WriteLine($" {preview}");
Console.WriteLine($" ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
Console.ResetColor();
return EventAction.Next;
}
public EventAction HandleStrategyReady(StrategyReadyEvent evt)
{
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.Blue;
if (evt.Data is StrategyAnalysis analysis)
{
Console.WriteLine($"\n ✓ Analiza strategiczna gotowa");
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.WriteLine($" Hook: \"{analysis.RecommendedHook}\"");
if (analysis.SuggestedHashtags.Length > 0)
Console.WriteLine($" Hashtagi: {string.Join(" ", analysis.SuggestedHashtags)}");
Console.WriteLine($" Uwagi: {analysis.EngagementNotes}");
}
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" ⏱️ Faza 2 (równoległa): {_phaseStopwatch.ElapsedMilliseconds}ms");
Console.ResetColor();
_phaseStopwatch.Restart();
return EventAction.Next;
}
// ── Framework event handlers ──────────────────────────────────────────────
public EventAction HandleError(ExecutorFailedEvent evt)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"\n❌ BŁĄD AGENTA: {evt.ExecutorId}");
Console.WriteLine($" {evt.Data}");
Console.ResetColor();
return EventAction.Break;
}
public EventAction HandleToken(AgentResponseUpdateEvent evt)
{
var name = ShortName(evt.ExecutorId);
if (_seenAgents.Add(evt.ExecutorId))
{
PrintParallelBanner(name);
PrintAgentHeader(name);
}
if (!string.IsNullOrEmpty(evt.Update?.Text))
Console.Write(evt.Update.Text);
return EventAction.Next;
}
// ── HITL handler ──────────────────────────────────────────────────────────
/// <summary>
/// Obsługuje RequestInfoEvent z RequestPort (workflow-level HITL).
/// Payload = ApprovalRequest.
/// </summary>
public async Task<EventAction> HandleHITL(RequestInfoEvent evt, StreamingRun run)
{
if (evt.Request.TryGetDataAs(out ApprovalRequest? approvalRequest)
&& approvalRequest is not null)
{
return await HandleWorkflowApproval(evt, run, approvalRequest);
}
return EventAction.Next;
}
private async Task<EventAction> HandleWorkflowApproval(
RequestInfoEvent evt, StreamingRun run, ApprovalRequest approvalRequest)
{
Console.ResetColor();
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("╔══════════════════════════════════════════════════════════════╗");
Console.WriteLine($"║ 🛑 WORKFLOW HITL : Zatwierdzenie fazy: {approvalRequest.PhaseName,-18}║");
Console.WriteLine("╚══════════════════════════════════════════════════════════════╝");
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"\n📋 Podsumowanie:");
Console.ForegroundColor = ConsoleColor.Cyan;
// Pełne podsumowanie : każda linia z wcięciem
foreach (var line in approvalRequest.Summary.Split('\n'))
{
Console.WriteLine($" {line}");
}
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write($"\n👉 Zatwierdzić fazę '{approvalRequest.PhaseName}'? (t/n, opcjonalnie feedback po spacji): ");
Console.ResetColor();
var input = Console.ReadLine()?.Trim() ?? "";
var parts = input.Split(' ', 2);
var answer = parts[0].ToLower();
bool approved = answer is "t" or "tak" or "y" or "yes";
string? feedback = parts.Length > 1 ? parts[1] : null;
var approvalDecision = new ApprovalDecision(approved, feedback);
await run.SendResponseAsync(
evt.Request.CreateResponse(approvalDecision));
Console.ForegroundColor = approved ? ConsoleColor.Green : ConsoleColor.Red;
Console.WriteLine(approved
? $"\n✅ Faza '{approvalRequest.PhaseName}' zatwierdzona!"
: $"\n❌ Faza '{approvalRequest.PhaseName}' odrzucona.");
Console.ResetColor();
return EventAction.Next;
}
public EventAction HandleOutput(WorkflowOutputEvent evt)
{
if (ShortName(evt.ExecutorId ?? "") == _finalAgentName)
_workflowOutputReceived = true;
return EventAction.Continue;
}
public EventAction HandleCompleted(ExecutorCompletedEvent evt)
{
var name = ShortName(evt.ExecutorId);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"\n ✔ {name} zakończony");
Console.ResetColor();
if (_workflowOutputReceived)
{
_stopwatch.Stop();
return EventAction.Break;
}
return EventAction.Next;
}
// ── Wyświetlanie ──────────────────────────────────────────────────────────
private void PrintParallelBanner(string agentName)
{
if (!_parallelBannerShown && _parallelAgents.Contains(agentName))
{
_parallelBannerShown = true;
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine("\n\n⚡ FAZA RÓWNOLEGŁA : Copywriter i Strateg startują jednocześnie:");
Console.ResetColor();
}
}
private static void PrintAgentHeader(string agentName)
{
var (color, icon, prefix) = GetStyle(agentName);
Console.WriteLine();
Console.WriteLine("────────────────────────────────────────────────────────────────");
Console.ForegroundColor = color;
Console.WriteLine($" {prefix} AGENT: {agentName} {icon}");
Console.WriteLine("────────────────────────────────────────────────────────────────");
Console.ResetColor();
}
public void PrintSummary()
{
Console.ResetColor();
Console.WriteLine();
Console.WriteLine("════════════════════════════════════════════════════════════════════");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"🏁 Pipeline zakończony! Łączny czas: {_stopwatch.ElapsedMilliseconds}ms");
Console.ResetColor();
Console.WriteLine("════════════════════════════════════════════════════════════════════");
Console.WriteLine("\n💡 Copywriter i Strateg startowali jednocześnie :");
Console.WriteLine(" czas fazy 2 = max(t_copywriter, t_strateg)");
// ── Audit report ──────────────────────────────────────────────
ApprovalAuditLog.PrintReport();
// ── Walidacja wszystkich faz HITL ─────────────────────────────
var requiredPhases = new[] { "Research", "Editorial", "Publish" };
if (ApprovalAuditLog.AllPhasesApproved(requiredPhases))
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\n✅ WALIDACJA: Wszystkie 3 fazy HITL zostały zatwierdzone.");
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("\n⚠️ WALIDACJA: Nie wszystkie fazy HITL zostały zatwierdzone!");
}
Console.ResetColor();
}
public static void PrintBanner()
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("╔════════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ 🚀 AI Content Team : v6c: Guaranteed HITL Pipeline ║");
Console.WriteLine("╠════════════════════════════════════════════════════════════════╣");
Console.WriteLine("║ [1] Researcher ║");
Console.WriteLine("║ ↓ ║");
Console.WriteLine("║ PrepareResearch → ResearchPort → GateResearch 🛑 HITL#1 ║");
Console.WriteLine("║ ↓ fan-out ║");
Console.WriteLine("║ [2a] Copywriter ══╗ RÓWNOLEGLE ⚡ ║");
Console.WriteLine("║ [2b] Strateg ══╝ ║");
Console.WriteLine("║ ↓ fan-in barrier → aggregator ║");
Console.WriteLine("║ PrepareEditorial → EditorialPort → GateEditorial 🛑 HITL#2║");
Console.WriteLine("║ ↓ ║");
Console.WriteLine("║ [3] Redaktor (edycja, bez publikacji) ║");
Console.WriteLine("║ ↓ ║");
Console.WriteLine("║ PreparePublish → PublishPort → GatePublish 🛑 HITL#3 ║");
Console.WriteLine("║ ↓ ║");
Console.WriteLine("║ [4] PublishExecutor (publikacja po zatwierdzeniu) ║");
Console.WriteLine("║ ║");
Console.WriteLine("║ Wszystkie HITL via RequestPort : GWARANCJA zatrzymania ║");
Console.WriteLine("║ Agent NIE MOŻE ominąć zatwierdzenia ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════════╝");
Console.ResetColor();
}
}
[VIZ] Mermaid zapisany: D:\PanNiebieski\Webinary\MSTECH_MicrosoftAgentFrameworkMain\AgentDemo4\bin\Debug\net10.0\workflow_viz\workflow_graph.mmd
[VIZ] DOT zapisany: D:\PanNiebieski\Webinary\MSTECH_MicrosoftAgentFrameworkMain\AgentDemo4\bin\Debug\net10.0\workflow_viz\workflow_graph.dot
[VIZ] Wszystkie pliki w: D:\PanNiebieski\Webinary\MSTECH_MicrosoftAgentFrameworkMain\AgentDemo4\bin\Debug\net10.0\workflow_viz
╔════════════════════════════════════════════════════════════════╗
║ 🚀 AI Content Team : v6c: Guaranteed HITL Pipeline ║
╠════════════════════════════════════════════════════════════════╣
║ [1] Researcher ║
║ ↓ ║
║ PrepareResearch → ResearchPort → GateResearch 🛑 HITL#1 ║
║ ↓ fan-out ║
║ [2a] Copywriter ══╗ RÓWNOLEGLE ⚡ ║
║ [2b] Strateg ══╝ ║
║ ↓ fan-in barrier → aggregator ║
║ PrepareEditorial → EditorialPort → GateEditorial 🛑 HITL#2║
║ ↓ ║
║ [3] Redaktor (edycja, bez publikacji) ║
║ ↓ ║
║ PreparePublish → PublishPort → GatePublish 🛑 HITL#3 ║
║ ↓ ║
║ [4] PublishExecutor (publikacja po zatwierdzeniu) ║
║ ║
║ Wszystkie HITL via RequestPort : GWARANCJA zatrzymania ║
║ Agent NIE MOŻE ominąć zatwierdzenia ║
╚════════════════════════════════════════════════════════════════╝
⏳ 🔍 Researcher: Pobieram transkrypcję z: https://youtube.com/watch?v=maf100demo
[MW:Log] ▶ Researcher start (1 wiadomości)
[MW:Audit] 🔧 Researcher → get_youtube_transcript()
[MW:Audit] ✓ get_youtube_transcript zakończony (25ms)
[MW:Log] ◀ Researcher zakończony (4842ms, 1037 znaków output)
┌─────────────────────────────────────────────────────────────┐
│ 🔍 RESEARCH ZAKOŃCZONY : 5 tez wyodrębnionych
└─────────────────────────────────────────────────────────────┘
• **MAF 1.0 to fuzja Semantic Kernel + AutoGen** : nowe framework łączy obie technologie w jedno stabilne API z SLA produkcyjnym gotowym do deploymentu w systemach krytycznych
• **Trzy typy workflowów ze streamingiem** : sequential, concurrent i handoff workflows umożliwiają budowanie zaawansowanych scenariuszy agentowych z obsługą realtime data flow
• **MCP jako pierwszoklasowy obywatel** : Model Context Protocol jest w pełni zintegrowany z dynamicznym odkrywaniem i zarządzaniem tools, eliminując boilerplate
• **Human-in-the-Loop przez ApprovalRequiredAIFunction** : jeden prosty wrapper pozwala implementować approval workflows i audyt decyzji AI bez skomplikowanej architektury
• **Agent Skills z progressive disclosure** : wiedza dziedzinowa w formacie SKILL.md umożliwia stopniowe ujawnianie możliwości agentów, ułatwiając maintainability i governance
⏱️ Faza 1 (Research): 0ms
✔ Researcher zakończony
⏳ 🛑 PrepareResearch: ⏸️ Czekam na zatwierdzenie fazy: Research
✔ PrepareResearch zakończony
╔══════════════════════════════════════════════════════════════╗
║ 🛑 WORKFLOW HITL : Zatwierdzenie fazy: Research ║
╚══════════════════════════════════════════════════════════════╝
📋 Podsumowanie:
URL: https://youtube.com/watch?v=maf100demo
Grupa docelowa: polscy .NET developerzy, seniorzy i architekci
Tezy (5):
• **MAF 1.0 to fuzja Semantic Kernel + AutoGen** : nowe framework łączy obie technologie w jedno stabilne API z SLA produkcyjnym gotowym do deploymentu w systemach krytycznych
• **Trzy typy workflowów ze streamingiem** : sequential, concurrent i handoff workflows umożliwiają budowanie zaawansowanych scenariuszy agentowych z obsługą realtime data flow
• **MCP jako pierwszoklasowy obywatel** : Model Context Protocol jest w pełni zintegrowany z dynamicznym odkrywaniem i zarządzaniem tools, eliminując boilerplate
• **Human-in-the-Loop przez ApprovalRequiredAIFunction** : jeden prosty wrapper pozwala implementować approval workflows i audyt decyzji AI bez skomplikowanej architektury
• **Agent Skills z progressive disclosure** : wiedza dziedzinowa w formacie SKILL.md umożliwia stopniowe ujawnianie możliwości agentów, ułatwiając maintainability i governance
👉 Zatwierdzić fazę 'Research'? (t/n, opcjonalnie feedback po spacji): t
✅ Faza 'Research' zatwierdzona!
✔ ResearchPort zakończony
✔ ResearchPort zakończony
✅ HITL Research: ZATWIERDZONO
✔ GateResearch zakończony
⏳ 📊 Strateg: Analizuję trendy i przygotowuję strategię...
[MW:Log] ▶ Strateg start (1 wiadomości)
[MW:Log] ▶ Copywriter start (1 wiadomości)
⏳ ✍️ Copywriter: Generuję draft posta na LinkedIn...
[MW:Audit] 🔧 Copywriter → load_skill()
[MW:Audit] ✓ load_skill zakończony (3ms)
[MW:Audit] 🔧 Copywriter → read_skill_resource()
[MW:Audit] ✓ read_skill_resource zakończony (2ms)
[MW:Audit] 🔧 Strateg → _Main_g_GetLinkedInTrends_0_4()
[MW:Audit] ✓ _Main_g_GetLinkedInTrends_0_4 zakończony (0ms)
[MW:Audit] 🔧 Strateg → _Main_g_GetLinkedInTrends_0_4()
[MW:Audit] ✓ _Main_g_GetLinkedInTrends_0_4 zakończony (0ms)
[MW:Audit] 🔧 Strateg → load_skill()
[MW:Audit] ✓ load_skill zakończony (0ms)
[MW:Audit] 🔧 Strateg → load_skill()
[MW:Audit] ✓ load_skill zakończony (0ms)
[MW:Log] ◀ Copywriter zakończony (11259ms, 1065 znaków output)
✓ Draft gotowy (1065 znaków)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
Teraz przeczytam branć voice, aby dostosować się do tonu:
Mam już wystarczające wytyczne. Napiszę post na podstawie tez i instrukcji z loadowanego skilla:
---
Właśnie przestał być eksperymentem. 🎯...
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
✔ Copywriter zakończony
[MW:Log] ◀ Strateg zakończony (12541ms, 2089 znaków output)
✓ Analiza strategiczna gotowa
Hook: "Pozwól, że najpierw sprawdzę aktualne trendy na LinkedIn w kategorii AI i Microsoft, a następnie załaduję niez"ędne umiejętności strategiczne.
Hashtagi: #dotnet #ai
Uwagi: Brak dodatkowych uwag.
⏱️ Faza 2 (równoległa): 38634ms
✔ Strateg zakończony
✔ Aggregator zakończony
✔ Aggregator zakończony
⏳ 🛑 PrepareEditorial: ⏸️ Czekam na zatwierdzenie fazy: Editorial
✔ PrepareEditorial zakończony
╔══════════════════════════════════════════════════════════════╗
║ 🛑 WORKFLOW HITL : Zatwierdzenie fazy: Editorial ║
╚══════════════════════════════════════════════════════════════╝
📋 Podsumowanie:
DRAFT (1065 znaków):
Teraz przeczytam branć voice, aby dostosować się do tonu:
Mam już wystarczające wytyczne. Napiszę post na podstawie tez i instrukcji z loadowanego skilla:
---
Właśnie przestał być eksperymentem. 🎯
MAF 1.0 to nie kolejny "nowoczesny framework" : to fuzja Semantic Kernel i AutoGen w jedno stabilne API z rzeczywistym SLA dla systemów krytycznych. Jeśli budowałeś agenty zaraz-po-zaraz fixując boilerplate, wiedz że się kończy.
Trzy typy workflowów (sequential, concurrent, handoff) pozwalają implementować rzeczy które wcześniej wymagały custom choreografii. MCP jest w pełni zintegrowany : nie definiujesz tools, framework je odkrywa. Human-in-the-Loop przez ApprovalRequiredAIFunction to jeden wrapper zamiast całej architektury audytu.
Ale najciekawsza część? Agent Skills w formacie SKILL.md. Progressive disclosure wiedzy domenowej. Wreszcie umiem kontrolować co agent wie i kiedy to wie : maintainability bez przebicia w głowę.
Tych zmian czekaliśmy. Czy już testowałeś MAF 1.0 w swoim projekcie? 🤔
#dotnet #csharp #ai #agentframework #semantickernel
STRATEGIA:
Hook: Pozwól, że najpierw sprawdzę aktualne trendy na LinkedIn w kategorii AI i Microsoft, a następnie załaduję niezbędne umiejętności strategiczne.
Hashtagi: #dotnet #ai
Uwagi: Brak dodatkowych uwag.
👉 Zatwierdzić fazę 'Editorial'? (t/n, opcjonalnie feedback po spacji): t
✅ Faza 'Editorial' zatwierdzona!
✔ EditorialPort zakończony
✔ EditorialPort zakończony
✅ HITL Editorial: ZATWIERDZONO
✔ GateEditorial zakończony
[MW:Log] ▶ Redaktor start (1 wiadomości)
⏳ 📝 Redaktor: Scala draft i strategię, przygotowuje finalny post...
[MW:Log] ◀ Redaktor zakończony (1824ms, 905 znaków output)
✔ Redaktor zakończony
⏳ 🛑 PreparePublish: ⏸️ Czekam na zatwierdzenie fazy: Publish
✔ PreparePublish zakończony
╔══════════════════════════════════════════════════════════════╗
║ 🛑 WORKFLOW HITL : Zatwierdzenie fazy: Publish ║
╚══════════════════════════════════════════════════════════════╝
📋 Podsumowanie:
FINALNY POST (zredagowany):
Właśnie przestał być eksperymentem. 🎯
MAF 1.0 to nie kolejny „nowoczesny framework" : to fuzja Semantic Kernel i AutoGen w jedno stabilne API z rzeczywistym SLA dla systemów krytycznych. Jeśli budowałeś agenty zaraz-po-zaraz fixując boilerplate, wiedz że się kończy.
Trzy typy workflowów (sequential, concurrent, handoff) pozwalają implementować rzeczy, które wcześniej wymagały custom choreografii. MCP jest w pełni zintegrowany : nie definiujesz tools, framework je odkrywa. Human-in-the-Loop przez ApprovalRequiredAIFunction to jeden wrapper zamiast całej architektury audytu.
Ale najciekawsza część? Agent Skills w formacie SKILL.md. Progressive disclosure wiedzy domenowej. Wreszcie umiem kontrolować, co agent wie i kiedy to wie : maintainability bez przebicia w głowę.
Tych zmian czekaliśmy. Czy już testowałeś MAF 1.0 w swoim projekcie? 🤔
#dotnet #ai #csharp #agentframework #semantickernel
Hashtagi: #dotnet #ai
Zredagowany o: 11:18:44
👉 Zatwierdzić fazę 'Publish'? (t/n, opcjonalnie feedback po spacji): t
✅ Faza 'Publish' zatwierdzona!
✔ PublishPort zakończony
✔ PublishPort zakończony
✅ HITL Publish: ZATWIERDZONO
✔ GatePublish zakończony
✅ POST OPUBLIKOWANY NA LINKEDIN:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Właśnie przestał być eksperymentem. 🎯
MAF 1.0 to nie kolejny „nowoczesny framework" : to fuzja Semantic Kernel i AutoGen w jedno stabilne API z rzeczywistym SLA dla systemów krytycznych. Jeśli budowałeś agenty zaraz-po-zaraz fixując boilerplate, wiedz że się kończy.
Trzy typy workflowów (sequential, concurrent, handoff) pozwalają implementować rzeczy, które wcześniej wymagały custom choreografii. MCP jest w pełni zintegrowany : nie definiujesz tools, framework je odkrywa. Human-in-the-Loop przez ApprovalRequiredAIFunction to jeden wrapper zamiast całej architektury audytu.
Ale najciekawsza część? Agent Skills w formacie SKILL.md. Progressive disclosure wiedzy domenowej. Wreszcie umiem kontrolować, co agent wie i kiedy to wie : maintainability bez przebicia w głowę.
Tych zmian czekaliśmy. Czy już testowałeś MAF 1.0 w swoim projekcie? 🤔
#dotnet #ai #csharp #agentframework #semantickernel
#dotnet #ai
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✔ Publish zakończony
════════════════════════════════════════════════════════════════════
🏁 Pipeline zakończony! Łączny czas: 53471ms
════════════════════════════════════════════════════════════════════
💡 Copywriter i Strateg startowali jednocześnie :
czas fazy 2 = max(t_copywriter, t_strateg)
┌─────────────────────────────────────────────────────────┐
│ 📋 AUDIT LOG : Historia zatwierdzeń HITL │
├─────────────────────────────────────────────────────────┤
│ ✅ Research 26072ms
│ ✅ Editorial 4047ms
│ ✅ Publish 4064ms
└─────────────────────────────────────────────────────────┘
✅ WALIDACJA: Wszystkie 3 fazy HITL zostały zatwierdzone.
┌─────────────────────────────────────────────────────────┐
│ 📊 MIDDLEWARE METRICS : Raport pipeline'u │
├─────────────────────────────────────────────────────────┤
│ Agent Runs: │
│ ✅ Researcher 4843ms │
│ ✅ Copywriter 11259ms │
│ ✅ Strateg 12541ms │
│ ✅ Redaktor 1824ms │
│ │
│ Function Calls: │
│ 🔧 Researcher/get_youtube_transcript 25ms │
│ 🔧 Copywriter/load_skill 4ms │
│ 🔧 Copywriter/read_skill_resource 3ms │
│ 🔧 Strateg/_Main_g_GetLinkedInTrends_0_4 1ms │
│ 🔧 Strateg/_Main_g_GetLinkedInTrends_0_4 0ms │
│ 🔧 Strateg/load_skill 0ms │
│ 🔧 Strateg/load_skill 0ms │
│ │
│ Tokeny (szacunkowo): │
│ Input: ~ 889 tokenów │
│ Output: ~ 1273 tokenów │
│ Razem: ~ 2162 tokenów │
└─────────────────────────────────────────────────────────┘
// ============================================================================
// AgentDevUIDemo/Program.cs : DevUI hosting dla AiContentTeam workflow
// ============================================================================
//
// Dwa workflow:
// content-pipeline-dev : bez HITL, do DevUI (Development)
// content-pipeline : pełny HITL, do produkcji (opcjonalnie)
//
// Znane ograniczenia DevUI w .NET:
// 1. Entry point workflow musi być ChatClientAgent (nie custom Executor)
// 2. ApprovalStage (HITL) nie działa : DevUI nie ma UI do zatwierdzania
//
// Uruchomienie:
// cd AgentDevUIDemo
// dotnet run
// # → http://localhost:5050/devui → wybierz "content-pipeline-dev"
//
// ============================================================================
using AiContentTeam;
using AiContentTeam.Guardrails;
using AiContentTeam.Middleware;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DevUI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
var builder = WebApplication.CreateBuilder(args);
// ── IChatClient ───────────────────────────────────────────────────────────────
IChatClient chatClient = new Anthropic.AnthropicClient()
.AsIChatClient("claude-haiku-4-5");
builder.Services.AddChatClient(chatClient);
// ── MCP tools ────────────────────────────────────────────────────────────────
builder.Services.AddSingleton<IList<McpClientTool>>(sp =>
{
return Task.Run(async () =>
{
var mcpClient = await McpClient.CreateAsync(
new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost:2001/mcp")
}));
return await mcpClient.ListToolsAsync();
}).GetAwaiter().GetResult();
});
// ── Skills provider ───────────────────────────────────────────────────────────
#pragma warning disable MAAI001
builder.Services.AddSingleton(
new AgentSkillsProvider(Path.Combine(AppContext.BaseDirectory, "skills")));
#pragma warning restore MAAI001
// ── Guardrails ────────────────────────────────────────────────────────────────
var contentPolicy = new ContentPolicyGuardrail();
var brandSafety = new BrandSafetyGuardrail(
competitors: ["CompetitorA", "CompetitorB"],
requireCTA: true);
var outputLength = new OutputLengthGuardrail(maxChars: 3000);
// ── Agenty w DI ───────────────────────────────────────────────────────────────
builder.Services.AddKeyedSingleton<AIAgent>("Researcher", (sp, _) =>
{
var chat = sp.GetRequiredService<IChatClient>();
var mcpTools = sp.GetRequiredService<IList<McpClientTool>>();
return GuardrailFactory.WrapWithFullPipeline(
new ChatClientAgent(chat, new ChatClientAgentOptions
{
Name = "Researcher",
ChatOptions = new()
{
Instructions = """
Jesteś research agentem. Twoim JEDYNYM zadaniem jest:
1. UŻYJ NARZĘDZIA GetYouTubeTranscript aby pobrać transkrypcję z podanego URL
2. Na podstawie wyniku wyodrębnij dokładnie 5 kluczowych tez jako bullet pointy
ZASADY:
- ZAWSZE wywołuj narzędzie GetYouTubeTranscript
- Odpowiadaj po polsku
- Każdą tezę pisz w osobnej linii zaczynającej się od "• "
""",
Tools = [.. mcpTools]
}
}),
enableLogging: true,
enableRetry: true);
});
builder.Services.AddKeyedSingleton<AIAgent>("Copywriter", (sp, _) =>
{
var chat = sp.GetRequiredService<IChatClient>();
var skills = sp.GetRequiredService<AgentSkillsProvider>();
return GuardrailFactory.WrapWithFullPipeline(
new ChatClientAgent(chat, new ChatClientAgentOptions
{
Name = "Copywriter",
AIContextProviders = [skills],
ChatOptions = new()
{
Instructions = """
Jesteś copywriterem specjalizującym się w treściach dla .NET developerów.
Na podstawie tez od Researchera napisz post na LinkedIn.
ZASADY:
- 150-250 słów, max 3 emoji
- Mocny hook w 1. zdaniu
- Zakończ pytaniem do społeczności
- Napisz TYLKO treść posta, bez meta-komentarza
- Odpowiadaj po polsku
"""
}
}),
enableLogging: true,
enableRetry: true,
contentPolicy, brandSafety, outputLength);
});
builder.Services.AddKeyedSingleton<AIAgent>("Strateg", (sp, _) =>
{
var chat = sp.GetRequiredService<IChatClient>();
var skills = sp.GetRequiredService<AgentSkillsProvider>();
return GuardrailFactory.WrapWithFullPipeline(
new ChatClientAgent(chat, new ChatClientAgentOptions
{
Name = "Strateg",
AIContextProviders = [skills],
ChatOptions = new()
{
Instructions = """
Jesteś strategiem social media dla społeczności .NET developerów.
Przeanalizuj temat i odpowiedz STRUKTURALNIE:
ANALIZA TRENDÓW: [2-3 zdania]
REKOMENDOWANY HOOK: [jedno zdanie]
SUGEROWANE HASHTAGI: [max 5, format #tag]
UWAGI O ZAANGAŻOWANIU: [1-2 zdania]
Odpowiadaj po polsku.
""",
Tools = [AIFunctionFactory.Create(GetLinkedInTrends)]
}
}),
enableLogging: true,
enableRetry: true,
outputLength);
});
builder.Services.AddKeyedSingleton<AIAgent>("Redaktor", (sp, _) =>
{
var chat = sp.GetRequiredService<IChatClient>();
return GuardrailFactory.WrapWithFullPipeline(
new ChatClientAgent(chat, new ChatClientAgentOptions
{
Name = "Redaktor",
ChatOptions = new()
{
Instructions = """
Jesteś redaktorem naczelnym. Dostałeś draft i analizę strategiczną.
Zastosuj rekomendacje, popraw błędy językowe.
Napisz TYLKO finalną treść posta po polsku.
"""
}
}),
enableLogging: true,
enableRetry: true,
contentPolicy, brandSafety, outputLength);
});
// ══════════════════════════════════════════════════════════════════════════════
// content-pipeline-dev : BEZ HITL, do DevUI
// ══════════════════════════════════════════════════════════════════════════════
builder.AddWorkflow("content-pipeline-dev", (sp, key) =>
{
var chat = sp.GetRequiredService<IChatClient>();
var researcherAgent = sp.GetRequiredKeyedService<AIAgent>("Researcher");
var copywriterAgent = sp.GetRequiredKeyedService<AIAgent>("Copywriter");
var strategAgent = sp.GetRequiredKeyedService<AIAgent>("Strateg");
var redaktorAgent = sp.GetRequiredKeyedService<AIAgent>("Redaktor");
// Entry point: ChatClientAgent (DevUI wymaga tego)
var entryAgent = new ChatClientAgent(chat, new ChatClientAgentOptions
{
Name = "InputParser",
ChatOptions = new()
{
Instructions = """
Jesteś parserem inputu. Użytkownik poda URL YouTube lub temat.
Wyodrębnij URL i powtórz go dokładnie.
Jeśli nie ma URL, powtórz temat dosłownie.
Nie dodawaj żadnych komentarzy ani wyjaśnień.
Odpowiedz JEDNYM zdaniem lub samym URL-em.
"""
}
});
// Bridge: string → ResearchRequest (bo ResearcherExecutor oczekuje ResearchRequest)
var inputBridge = new InputBridgeExecutor();
// Executory
var researcher = new ResearcherExecutor(researcherAgent);
var copywriter = new CopywriterExecutor(copywriterAgent);
var strateg = new StrategExecutor(strategAgent);
var aggregator = new AggregatorExecutor();
var redaktor = new RedaktorExecutor(redaktorAgent);
var publisher = new PublishExecutor();
IEnumerable<ExecutorBinding> fanInSources = [copywriter, strateg];
// Graf: prosty, bez HITL
// W workflow zamiast:
// entryAgent → inputBridge → researcher(executor)
// Daj:
// entryAgent → researcherAgent(ChatClientAgent) → copywriter + strateg
return new WorkflowBuilder(entryAgent)
.WithName(key)
.AddEdge(entryAgent, researcherAgent) // ← sam agent, nie executor
.AddFanOutEdge(researcherAgent, [copywriterAgent, strategAgent])
.AddFanInBarrierEdge(
(IEnumerable<ExecutorBinding>)[copywriterAgent, strategAgent],
redaktorAgent)
.WithOutputFrom(redaktorAgent)
.Build();
}).AddAsAIAgent();
// ── OpenAI + DevUI ────────────────────────────────────────────────────────────
builder.Services.AddOpenAIResponses();
builder.Services.AddOpenAIConversations();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
// ── Build & Map ───────────────────────────────────────────────────────────────
var app = builder.Build();
// Middleware na wyjątki : PRZED endpointami
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[EXCEPTION] {ex}");
Console.ResetColor();
throw;
}
});
app.MapOpenAIResponses();
app.MapOpenAIConversations();
if (app.Environment.IsDevelopment())
{
app.MapDevUI();
}
app.MapGet("/health", () => Results.Ok(new
{
status = "healthy",
pipeline = "AiContentTeam : DevUI",
devui = "/devui",
features = new[] { "No HITL (dev)", "Middleware", "Guardrails", "Fan-out/Fan-in" }
}));
// ── Start ─────────────────────────────────────────────────────────────────────
app.Urls.Add("http://localhost:5050");
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ 🚀 AiContentTeam : DevUI Server ║");
Console.WriteLine("║ ║");
Console.WriteLine("║ DevUI: http://localhost:5050/devui ║");
Console.WriteLine("║ Health: http://localhost:5050/health ║");
Console.WriteLine("║ ║");
Console.WriteLine("║ Workflow: content-pipeline-dev (no HITL) ║");
Console.WriteLine("║ ║");
Console.WriteLine("║ Pipeline: ║");
Console.WriteLine("║ InputParser → Bridge → Researcher ║");
Console.WriteLine("║ → Copywriter + Strateg (fan-out) ║");
Console.WriteLine("║ → Aggregator → Redaktor → Publisher ║");
Console.WriteLine("║ ║");
Console.WriteLine("║ Middleware: Logging, Retry, Guardrails, Function Audit ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
Console.ResetColor();
app.Run();
// ── Tool helper ───────────────────────────────────────────────────────────────
[System.ComponentModel.Description("Sprawdza popularne trendy na LinkedIn w kategorii tech/AI")]
static string GetLinkedInTrends(
[System.ComponentModel.Description("Kategoria, np. 'AI', '.NET'")] string category)
{
return $"""
Top trendy w kategorii "{category}" (symulacja):
1. Microsoft Agent Framework : wysoka aktywność
2. .NET 10 Preview : umiarkowane zaangażowanie
3. Multi-agent orchestration : rosnący trend
""";
}
// ══════════════════════════════════════════════════════════════════════════════
// InputBridgeExecutor : konwertuje string z ChatClientAgent na ResearchRequest
// ══════════════════════════════════════════════════════════════════════════════
/// <summary>
/// Bridge między ChatClientAgent (zwraca string) a ResearcherExecutor (oczekuje ResearchRequest).
/// Potrzebny bo DevUI wymaga ChatClientAgent jako entry point workflow.
/// Produkcyjny Program.cs (konsolowy) nie potrzebuje tego : tam InputParserExecutor
/// jest entry pointem i bezpośrednio tworzy ResearchRequest.
/// </summary>
public partial class InputBridgeExecutor() : Executor<string, ResearchRequest>("InputBridge")
{
public override ValueTask<ResearchRequest> HandleAsync(
string message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"[InputBridge] Parsing: '{message}'");
return ValueTask.FromResult(new ResearchRequest(
YouTubeUrl: message.Trim(),
TargetAudience: "polscy .NET developerzy, seniorzy i architekci"));
}
}
| Aspekt | Semantic Kernel | Agent Framework |
|---|---|---|
| Centralny obiekt | Kernel (ciężki orkiestrator) |
Brak — agenty i workflow bezpośrednio |
| Rejestracja narzędzi | Plugin → Kernel → [KernelFunction] |
AIFunctionFactory.Create(metoda) |
| Abstrakcja modelu | Własne konektory SK | IChatClient z Microsoft.Extensions.AI |
| Workflow | Brak natywnego (planner, ale bez grafu) | Graph-based WorkflowBuilder z executorami |
| HITL | Brak natywnego mechanizmu | RequestPort z gwarancją zatrzymania |
| Multi-agent | Eksperymentalne AgentGroupChat |
Natywne orkiestracje: Sequential, Concurrent, GroupChat, Handoff, Magentic |
| Microsoft Learn : dokumentacja (1/2) | Link |
|---|---|
| Przegląd frameworka | learn.microsoft.com/en-us/agent-framework/overview/ |
| Przegląd (pivot C#) | learn.microsoft.com/en-us/agent-framework/overview/?pivots=programming-language-csharp |
| Workflows overview | learn.microsoft.com/en-us/agent-framework/workflows/ |
| Human-in-the-Loop docs | learn.microsoft.com/en-us/agent-framework/workflows/human-in-the-loop |
| HITL (pivot C#) | learn.microsoft.com/en-us/agent-framework/workflows/human-in-the-loop?pivots=…csharp |
| Edges w grafie workflow | learn.microsoft.com/en-us/agent-framework/workflows/edges |
| WorkflowBuilder docs | learn.microsoft.com/en-us/agent-framework/workflows/workflows |
| Agent Skills docs | learn.microsoft.com/en-us/agent-framework/agents/skills |
| Tool approval / HITL | learn.microsoft.com/en-us/agent-framework/agents/tools/tool-approval?pivots=…csharp |
| Agent Middleware docs | learn.microsoft.com/en-us/agent-framework/user-guide/agents/agent-middleware |
| Microsoft Learn : dokumentacja (2/2) | Link |
|---|---|
| HITL w orkiestracjach | learn.microsoft.com/…/workflows/orchestrations/human-in-the-loop |
| MCP tools w agencie | learn.microsoft.com/…/model-context-protocol/using-mcp-tools |
| Middleware termination | learn.microsoft.com/en-us/agent-framework/agents/middleware/termination |
| Dodawanie middleware | learn.microsoft.com/en-us/agent-framework/journey/adding-middleware |
| Migracja z Semantic Kernel | learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel/ |
| AG-UI getting started C# | learn.microsoft.com/…/integrations/ag-ui/getting-started?pivots=…csharp |
| AG-UI overview | learn.microsoft.com/en-us/agent-framework/integrations/ag-ui/ |
| DevUI docs | learn.microsoft.com/en-us/agent-framework/devui/ |
| Hosting / ASP.NET setup | learn.microsoft.com/en-us/agent-framework/get-started/hosting |
| MAF hosting / deployment (DeepWiki) | deepwiki.com/microsoft/agent-framework/5-deployment-and-hosting |
| GitHub : repozytoria i kod (1/2) | Link |
|---|---|
| Główne repo MAF | github.com/microsoft/agent-framework |
| Repo z przykładami | github.com/microsoft/Agent-Framework-Samples |
| Sample: Agents | github.com/…/dotnet/samples/02-agents/Agents |
| Sample: AgentProviders | github.com/…/dotnet/samples/02-agents/AgentProviders |
| Sample: Workflows | github.com/…/dotnet/samples/03-workflows |
| Sample: Workflow Viz | github.com/…/dotnet/samples/03-workflows/Visualization |
| Sample: DevUI setup | github.com/…/dotnet/samples/GettingStarted/DevUI |
| Sample: DevUI Program.cs | github.com/…/GettingStarted/DevUI/Program.cs |
| Sample: Declarative Agent | github.com/…/agent-samples/chatclient/GetWeather.yaml |
| Sample: Multi-turn chat | github.com/…/01-get-started/03_multi_turn/Program.cs |
| GitHub : repozytoria i kod (2/2) | Link |
|---|---|
| Sample: Agent z Memory | github.com/…/01-get-started/04_memory/Program.cs |
| Sample: Middleware | github.com/…/02-agents/Agents/Agent_Step11_Middleware |
| Sample: Tool Approvals | github.com/…/02-agents/Agents/Agent_Step01… |
| Sample: Agent + MCP | github.com/…/02-agents/ModelContextProtocol/Agent_MCP |
| Sample: Agent Skills | github.com/…/02-agents/AgentSkills/Agent_Step01_BasicSkil |
| Sample: Agent Workflow | github.com/…/03-workflows/_StartHere/03_AgentWorkflow |
| Sample: Custom Executors | github.com/…/CustomAgentExecutors/Program.cs |
| Sample: A2A Server | github.com/…/A2AClientServer/A2AServer/Program.cs |
| Sample: A2A Client | github.com/…/A2AClientServer/A2AClient/Program.cs |
| Issue: DevUI + executory | github.com/microsoft/agent-framework/issues/2084 |
| Issue: WorkflowOutput bug | github.com/microsoft/agent-framework/issues/2691 |
| Dyskusja: DevUI workflow | github.com/microsoft/agent-framework/discussions/2531 |
| Gist: DevUI wzorzec | gist.github.com/wullemsb/70fae615a805dbfadbc51e53ea739101 |
| NuGet : pakiety | Link |
|---|---|
| Core agentów MAF | nuget.org/packages/Microsoft.Agents.AI |
| Workflow engine MAF | nuget.org/packages/Microsoft.Agents.AI.Workflows |
| Source generator Roslyn | nuget.org/packages/Microsoft.Agents.AI.Workflows.Generators |
| ASP.NET hosting MAF | nuget.org/packages/Microsoft.Agents.AI.Hosting |
| DevUI debugger paczka | nuget.org/packages/Microsoft.Agents.AI.DevUI |
| AG-UI ASP.NET hosting | nuget.org/packages/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore |
| Abstrakcja IChatClient | nuget.org/packages/Microsoft.Extensions.AI |
| MEAI adapter do OpenAI | nuget.org/packages/Microsoft.Extensions.AI.OpenAI |
| MCP SDK dla C# | nuget.org/packages/ModelContextProtocol |
| Klient OpenAI SDK | nuget.org/packages/OpenAI |
| Klient Ollama (lokalne LLM) | nuget.org/packages/OllamaSharp |
| Klient Anthropic/Claude | nuget.org/packages/Anthropic |
| Wizualizacja grafów DOT | nuget.org/packages/Graphviz |