# Feature: AI Chat Agent with Streaming & Rich Text ## Target: Existing MudBlazor app (.NET 9 / Blazor WASM + ASP.NET Core API) ## Includes: basic-chat-interface, wire-responses-api, migrate-to-semantic-kernel, multi-turn-conversations, add-test-coverage, enable-rich-text-display ## Skipped: migrate-claude-md-to-openspec (project scaffolding only) ## Integration Rule This feature is **additive only**. Existing applicationX code takes precedence over this spec in all cases. - **DO NOT** modify existing files, components, layouts, services, or patterns in the target - **DO NOT** replace existing patterns (e.g., if the target uses a different HttpClient pattern, use theirs) - **DO** add new files, new nav links, new routes, new DI registrations - **DO** conform to the target's existing code style, naming, and project structure - If the target already has an equivalent service (markdown renderer, HTTP wrapper, etc.), **use theirs** - If a task conflicts with existing target code, **stop and notify the user** — do not skip silently; the user decides whether to skip, adapt, or redesign ## Assumes - .NET 9 solution with Blazor WASM client + ASP.NET Core API projects + Shared class library - MudBlazor 9.2.0 installed and configured (AddMudServices, providers in MainLayout) - MudLayout with MudAppBar and MudMainContent in MainLayout.razor ## Target Layout (CRC app) ``` |------ ~220px ------|-------------- fluid ---------------| | | ≡ CRC 0.0.0 APR-CRC-PROD-... | ← MudAppBar (regular, ~64px) | Home |------------------------------------| | Pricer | | | Market Data | MudMainContent (@Body) | | XVA Statistics | ← Chat page renders here | | Sales | | | Sales Assistant ★ | | | | | | MudDrawer (~220px) | | |---------------------|-----------------------------------| ``` - MudAppBar: **regular height (~64px)**, not Dense — hamburger toggle, title + version, env badge - MudDrawer: left, **~220px**, toggled by hamburger, contains MudNavMenu - MudMainContent: fluid width, pages render via @Body - "Sales Assistant" added as new MudNavLink in existing MudNavMenu - Chat page renders **inside MudMainContent** — must not set its own AppBar or layout - Available viewport: `height: calc(100vh - 64px)`, width: fluid minus drawer when open ## Packages **API project:** - `Microsoft.SemanticKernel` 1.74.0 - `Microsoft.SemanticKernel.Connectors.OpenAI` 1.74.0 **Client project:** - `Microsoft.Extensions.Http` 9.0.4 - `Markdig` 1.1.1 **Test projects (xUnit):** - `xunit`, `xunit.runner.visualstudio`, `Microsoft.NET.Test.Sdk` - API tests: `Moq`, `Microsoft.AspNetCore.Mvc.Testing` - Client tests: `Moq` ## Architecture Three-project solution: WASM client sends chat requests to ASP.NET Core API, which processes them through Semantic Kernel (pointed at an OpenAI-compatible endpoint) and streams token deltas back as SSE. Client renders assistant markdown as sanitized HTML. An extraction plugin demonstrates SK tool calling. ## Shared Models ### `ChatMessage` — Shared/Models/ - `string Role` ("user" | "assistant"), `string Content`, `DateTime Timestamp` ### `ChatRequest` — Shared/Models/ - `List Messages` ### `HealthResponse` — Shared/Models/ - `string Status`, `DateTime Timestamp` ### `ExtractedFields` — Shared/Models/ - Required: `string? Client`, `string? Project`, `decimal? Hours`, `decimal? Rate`, `string? Currency`, `string? Date` - Optional: `string? Description`, `string? PoNumber` ### `ValidationResult` — Shared/Models/ - `bool IsValid`, `List Errors` ## Components ### API: `Program.cs` - Register `AddControllers()`, `AddOpenAIChatCompletion()` with configurable endpoint, `AddKernel()`, ExtractionPlugin singleton - CORS policy allowing Blazor client origin, any header/method - Config keys: `ResponsesApi:BaseUrl` (default `http://localhost:8317/v1`), `ResponsesApi:Model`, `ResponsesApi:ApiKey` - IMPORTANT: Base URL must include `/v1` — the OpenAI SDK appends `chat/completions` directly - Add `public partial class Program { }` for test accessibility ### API: `ChatController` — Controllers/ - POST `/api/chat` accepts `ChatRequest`, returns `text/event-stream` - Get `IChatCompletionService` from Kernel, convert messages to `ChatHistory` - Import ExtractionPlugin from DI: `_kernel.ImportPluginFromObject(plugin, "Extraction")` - Use `OpenAIPromptExecutionSettings` with `FunctionChoiceBehavior.Auto()` - Stream via `GetStreamingChatMessageContentsAsync()`, emit non-empty chunks as SSE - SSE format: `data: {"text":""}\n\n`, completion: `data: [DONE]\n\n`, error: `data: {"error":""}\n\n` - Catch `HttpRequestException` → error SSE event, `TaskCanceledException` → silent (client disconnect) ### API: `HealthController` — Controllers/ - GET `/api/health` → returns `HealthResponse` with status "Healthy" and UTC timestamp ### API: `ExtractionPlugin` — Plugins/ - `[KernelFunction("validate_extracted_fields")]` method accepting JSON string - Deserialize to `ExtractedFields`, validate required fields non-null, Hours/Rate > 0 - Return `ValidationResult` as JSON string ### Client: `Program.cs` - Register `AddMudServices()`, `MarkdownService` (singleton) - Register `AddHttpClient` with API base URL from config - Config: `ApiBaseUrl_Https`, `ApiBaseUrl_Http` in wwwroot/appsettings.json - Auto-detect HTTPS from `HostEnvironment.BaseAddress` ### Client: `ChatApiClient` — Services/ - Typed HttpClient wrapper - `GetHealthAsync()` → GET api/health, returns `HealthResponse?` - `SendChatStreamingAsync(ChatRequest)` → `IAsyncEnumerable` - Build `HttpRequestMessage` manually - Call `httpRequest.SetBrowserResponseStreamingEnabled(true)` (Blazor WASM extension) - Send with `HttpCompletionOption.ResponseHeadersRead` - Parse SSE line-by-line: extract `"text"` field, throw on `"error"` field, stop on `[DONE]` - SSE loop: see "Critical Patterns" section below — do NOT use `reader.EndOfStream` ### Client: `MarkdownService` — Services/ - Singleton wrapping Markdig with `UseAdvancedExtensions()` pipeline - `ConvertToHtml(string markdown)` → sanitized HTML string - Two-pass sanitization: 1. Strip `