Files
AgenticCode/openspec/exports/nlxva-pricer-openspec.md
local 956ec243c5 fix: update export bundle for Azure OpenAI and add streaming diagnostics
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>
2026-04-07 01:42:38 +01:00

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
```