# Natural Language XVA Pricer — OpenSpec Bundle for CRC ## Setup Instructions On the CRC target machine: 1. **Create the change directory:** ``` mkdir -p openspec/changes/nlxva-pricer ``` 2. **Save these files** into `openspec/changes/nlxva-pricer/`: - `proposal.md` (below) - `design.md` (below) - `tasks.md` (below) - `.openspec.yaml` (below) 3. **Copy the portable spec** `nlxva-pricer-spec.md` somewhere accessible (e.g., `openspec/changes/nlxva-pricer/reference-spec.md`) 4. **Copy the examples folder** to CRC.Server project root: ``` examples/extraction/ ├── instruction-template.txt └── few-shot/01/ 02/ 03/ (each with input.html + output.json) ``` 5. **Run:** `/opsx:apply nlxva-pricer` — reference the portable spec when implementing each task --- ## .openspec.yaml ```yaml name: nlxva-pricer title: Natural Language XVA Pricer status: active created: 2026-04-07 specs: - nlxva-pricer ``` --- ## proposal.md ```markdown # Add Natural Language XVA Pricer ## What Add an AI-powered chat page to CRC that can: 1. Accept natural language queries about XVA pricing 2. Accept email uploads (.html) and autonomously extract structured trade data (TradeItem JSON) 3. Validate extracted data via external API tool calls (counterparty lookup, trade/currency validation) 4. Handle disambiguation (multiple counterparty matches) via conversational follow-up 5. Stream responses token-by-token with markdown rendering ## Why Sales/CVA desk currently manually reads pricing request emails and re-types trade data. This feature automates extraction with AI + tool-calling validation, reducing errors and time. ## Scope - New page at /nlxva-pricer, new MudNavLink in existing NavMenu - New controller with 2 endpoints (chat + extract), same SSE streaming contract - Semantic Kernel integration with Azure OpenAI (Azure AD auth via tenant ID) - Few-shot prompting infrastructure (instruction template + 3 examples) - External API clients for counterparty/trade/currency validation - Client-side markdown rendering with XSS sanitization - Email drag-and-drop and file picker upload - System prompt and model settings tabs for iteration ## Non-goals - No Fluxor state management (local component state is sufficient for this isolated page) - No conversation persistence (in-memory only, lost on refresh) - No auth changes (inherits CRC's existing auth) ``` --- ## design.md ```markdown # NL XVA Pricer — Design ## Architecture Decision: Semantic Kernel over raw HttpClient **Why:** SK provides automatic function calling — the LLM can invoke validation tools (lookup_counterparty, validate_trade, etc.) autonomously and reason about results. With raw HttpClient we'd need to manually parse tool-call JSON, dispatch functions, and feed results back. SK handles this loop automatically via FunctionChoiceBehavior.Auto(). ## Architecture Decision: Azure OpenAI with DefaultAzureCredential **Why:** The sandbox environment uses Azure OpenAI with an Azure AD tenant ID. SK's `AddAzureOpenAIChatCompletion()` with `DefaultAzureCredential` integrates with CRC's existing Azure AD auth. No API keys to manage — uses the developer's `az login` token locally and managed identity in production. The endpoint URL does NOT need `/v1` (Azure SDK constructs the path internally). ## Architecture Decision: SSE streaming over WebSocket **Why:** SSE is simpler (unidirectional server→client), works through HTTP proxies, and matches the OpenAI API's native streaming format. WebSocket would add complexity (connection management, reconnection logic) with no benefit for this use case. The client only sends complete requests via POST; streaming is server→client only. ## Architecture Decision: Typed HttpClient per external API **Why:** Each external API (counterparty, trade, currency) gets its own typed HttpClient registered via AddHttpClient(). This gives each client its own base URL, keeps concerns separated, and makes testing easy (mock one client without affecting others). IHttpClientFactory manages socket lifetimes and avoids exhaustion. ## Architecture Decision: Per-request plugin import (not global registration) **Why:** ExtractionPlugin depends on scoped services (typed HttpClients). Registering it globally on the Kernel at startup would capture stale references. Instead, we resolve from DI per-request and import into the Kernel. ## Architecture Decision: FewShotService as singleton with clone-on-use **Why:** Loading examples from disk is IO-bound and slow. Do it once at startup, cache the assembled ChatHistory prefix, and clone per request. The clone is a shallow copy (ChatHistory messages are immutable value objects) so it's fast. ## Architecture Decision: Markdown render caching **Why:** During streaming, StateHasChanged() fires on every token. Without caching, Markdig re-processes ALL prior messages each time. With a Dictionary cache, only the actively streaming message re-renders. Completed messages serve cached HTML. ## Architecture Decision: Component-local state (no Fluxor) **Why:** This is a self-contained page with no cross-page state sharing needs. Adding Fluxor actions/reducers/effects would be overengineering. The conversation list, streaming flag, extraction mode, and settings all live as private fields in the Razor component. CRC's existing Fluxor infrastructure is untouched. ## Risk: CORS CRC.Server may need its CORS policy updated to allow SSE streaming (Content-Type: text/event-stream) to the CRC.Client origin. Verify existing policy covers this. ## Risk: SSE response buffering Two streaming hops: Azure OpenAI → CRC.Server → Browser. Buffering at any point kills streaming UX. Common culprits: response compression middleware (`UseResponseCompression()`), reverse proxies (NGINX, IIS), Azure API Management in front of Azure OpenAI. Use the diagnostic stream-test endpoint (see Critical Pattern #8 in reference spec) to verify both hops stream correctly before building the UI. ## Risk: Semantic Kernel version compatibility CRC targets .NET 8.0. Ensure the SK NuGet package version is compatible with .NET 8. Current stable SK packages support .NET 8+. Also need `Microsoft.SemanticKernel.Connectors.AzureOpenAI` and `Azure.Identity` packages. ## Risk: Azure AD token acquisition `DefaultAzureCredential` tries multiple auth methods in sequence. On a developer machine, it uses Azure CLI login (`az login --tenant `). If the developer hasn't run `az login`, SK will fail with an auth error at the first LLM call, not at startup. ## Risk: Large file uploads Email HTML files are read entirely into memory (max 10MB guard). For typical sales emails (< 100KB) this is fine. The guard prevents accidental large file uploads. ``` --- ## tasks.md ```markdown # NL XVA Pricer — Implementation Tasks ## Phase 1: Foundation (Server) - [ ] **T1: Add NuGet packages** — Add `Microsoft.SemanticKernel`, `Microsoft.SemanticKernel.Connectors.AzureOpenAI`, and `Azure.Identity` to CRC.Server. Add `Markdig` 1.1.1 to CRC.Client (if not already present). CRC may already have `Azure.Identity` — check first. Verify .NET 8 compatibility. - [ ] **T2: Add shared DTOs** — Create in CRC.Shared: `NlxvaChatMessage`, `NlxvaChatRequest`, `NlxvaModelSettings`, `NlxvaExtractionRequest`, `NlxvaExtractionResult`, `TradeItem` (with `[JsonPropertyName]` snake_case), `NlxvaValidationResult`, `NlxvaCandidateMatch`. See Contracts section in reference spec for exact shapes. - [ ] **T3: Add external API clients** — Create in CRC.Server/Services: `CounterpartyApiClient`, `TradeApiClient`, `CurrencyApiClient`. Each is a typed HttpClient with a single async method. Register via `AddHttpClient()` in Program.cs/Startup.cs with base URLs from appsettings.json `ExternalApis` section. - [ ] **T4: Add ExtractionPlugin** — Create in CRC.Server/Plugins: `ExtractionPlugin` with 4 `[KernelFunction]` methods: `lookup_counterparty`, `validate_trade`, `validate_currency`, `validate_schema`. Each returns serialized JSON string. Register as Scoped in DI. See Critical Patterns #5 and #6 in reference spec. - [ ] **T5: Add FewShotService** — Create in CRC.Server/Services: `FewShotService` that loads instruction template + few-shot examples from disk. Caches ChatHistory prefix. Methods: `CloneWithEmail()`, `CloneWithEmailAndMessages()`. Register as Singleton. Copy examples/ folder to CRC.Server root. - [ ] **T6: Register Semantic Kernel** — In CRC.Server DI: `AddAzureOpenAIChatCompletion()` with `DefaultAzureCredential` (tenant ID from config) + `AddKernel()`. Endpoint is Azure OpenAI resource URL (NO `/v1`). Use deployment name, NOT model name. Config from `NlxvaPricer:*` keys in appsettings.json. See Critical Pattern #2. - [ ] **T6b: Verify streaming hop 1** — Add temporary `stream-test` diagnostic endpoint (see Critical Pattern #8). Run `curl -N` against it. Verify timestamps are spread across seconds (not clustered). Check for response compression middleware interference. Remove diagnostic endpoint after verification. - [ ] **T7: Add NlxvaPricerController** — Create controller with `POST /api/nlxva-pricer/chat` and `POST /api/nlxva-pricer/extract`. Both stream SSE. Chat endpoint: builds ChatHistory from messages + optional system prompt + model settings. Extract endpoint: uses FewShotService prefix. Both import ExtractionPlugin per-request and enable `FunctionChoiceBehavior.Auto()`. See Critical Pattern #6. ## Phase 2: Client - [ ] **T8: Add MarkdownService** — Create in CRC.Client/Services: `MarkdownService` with Markdig pipeline + HTML sanitization (tag/attribute allowlist). Register as Singleton. - [ ] **T9: Add NlxvaPricerApiClient** — Create in CRC.Client/Services: typed HttpClient with `SendChatStreamingAsync()` and `SendExtractionStreamingAsync()`. MUST use `SetBrowserResponseStreamingEnabled(true)` + `HttpCompletionOption.ResponseHeadersRead` + ReadLineAsync loop (NOT EndOfStream). See Critical Pattern #1 and #7. - [ ] **T10: Add file-drop.js** — Create `CRC.Client/wwwroot/js/file-drop.js`. Registers drag/drop handlers on a CSS-selector target, reads file as text, calls back to .NET via DotNetObjectReference. Add `