Replace CLIProxyAPI/local proxy references with Azure OpenAI using DefaultAzureCredential and tenant ID auth. Add Critical Pattern #8 for SSE buffering diagnostics with timestamped curl test. Add streaming verification tasks (T6b, T15) and troubleshooting entries for Azure AD auth, RBAC, response compression, and proxy buffering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
224 lines
12 KiB
Markdown
224 lines
12 KiB
Markdown
# 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<T>(). 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<Message, string>
|
|
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 <tenant-id>`). 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<T>()` 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 `<script>` reference in index.html.
|
|
|
|
- [ ] **T11: Add NlxvaPricer.razor page** — Create page at `@page "/nlxva-pricer"`. MudTabs with 3 panels (Chat, System Prompt, Model Settings). Message list with user/assistant bubbles, streaming indicator, markdown rendering. Email upload via drag-drop + InputFile. Extraction mode tracking and routing. See reference spec for full component behavior. CSS: use `calc(100vh - 64px)` for CRC's regular AppBar (NOT 48px). See Critical Pattern #3.
|
|
|
|
- [ ] **T12: Add NlxvaPricer.razor.css** — Scoped styles: tab-container, chat-container, message-list, message bubbles, input area, markdown-body styles, drag-over feedback, extraction indicator. All use `::deep` where targeting MudBlazor child markup.
|
|
|
|
- [ ] **T13: Add NavMenu link** — Add `<MudNavLink Href="/nlxva-pricer" Icon="@Icons.Material.Filled.SmartToy">NL XVA Pricer</MudNavLink>` to CRC's existing NavMenu component.
|
|
|
|
## Phase 3: Verify
|
|
|
|
- [ ] **T14: Config** — Add `NlxvaPricer` (AzureOpenAIEndpoint, DeploymentName, TenantId, FewShotPath) and `ExternalApis` sections to CRC.Server appsettings.json. Ensure CORS allows CRC.Client origin for SSE responses. Ensure developer has run `az login --tenant <tenant-id>`.
|
|
|
|
- [ ] **T15: Verify streaming end-to-end** — Run `curl -N` against `/api/nlxva-pricer/chat` to verify hop 2 (server → client) streams correctly. Check browser Network tab EventStream view for incremental token delivery. If response compression is enabled, verify SSE endpoints opt out.
|
|
|
|
- [ ] **T16: Smoke test** — Build both projects. Navigate to /nlxva-pricer. Send a chat message → verify streaming tokens appear incrementally. Upload an example email HTML → verify extraction streams. Verify New Chat resets. Verify drag-drop visual feedback.
|
|
|
|
## Implementation Notes
|
|
|
|
- Reference the portable spec (`nlxva-pricer-spec.md`) for exact contracts, critical patterns, and CSS values
|
|
- Adapt all new code to CRC naming conventions (E-prefix enums, I-prefix interfaces, *Dto suffixes)
|
|
- Do NOT modify any existing CRC files except: NavMenu (add one link), index.html (add one script tag), Program.cs/Startup.cs (add DI registrations), appsettings.json (add config sections)
|
|
- If any task conflicts with existing CRC patterns, STOP and consult the user
|
|
```
|