From d46b179221d5f5cad1482f850d55f325fbd9502f Mon Sep 17 00:00:00 2001 From: local Date: Tue, 7 Apr 2026 00:45:47 +0100 Subject: [PATCH] feat: add porting-guide skill and NL XVA Pricer export bundle Add /opsx:porting-guide skill that generates detailed human-readable implementation guides as a companion to /opsx:export-spec. The AI spec targets the agent; the porting guide targets the human developer with design rationale, task-by-task notes, troubleshooting tables, and rollback plans. Generate the full NL XVA Pricer export bundle for CRC: - nlxva-pricer-spec.md (AI-targeted portable spec) - nlxva-pricer-openspec.md (OpenSpec proposal/design/tasks) - nlxva-pricer-porting-guide.md (human implementation guide) Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/commands/opsx/porting-guide.md | 233 ++++++ openspec/exports/nlxva-pricer-openspec.md | 196 +++++ .../exports/nlxva-pricer-porting-guide.md | 671 ++++++++++++++++++ openspec/exports/nlxva-pricer-spec.md | 512 +++++++++++++ 4 files changed, 1612 insertions(+) create mode 100644 .claude/commands/opsx/porting-guide.md create mode 100644 openspec/exports/nlxva-pricer-openspec.md create mode 100644 openspec/exports/nlxva-pricer-porting-guide.md create mode 100644 openspec/exports/nlxva-pricer-spec.md diff --git a/.claude/commands/opsx/porting-guide.md b/.claude/commands/opsx/porting-guide.md new file mode 100644 index 0000000..b188860 --- /dev/null +++ b/.claude/commands/opsx/porting-guide.md @@ -0,0 +1,233 @@ +--- +name: "OPSX: Porting Guide" +description: Generate a detailed human-readable implementation guide for porting a feature to a target codebase +category: Workflow +tags: [workflow, portability, experimental] +--- + +Generate a detailed human-readable porting guide for implementing an exported feature on a target codebase. + +This is a **companion** to `/opsx:export-spec`. The export-spec is optimized for AI consumption (compact, precise). This guide is optimized for the **human developer** who needs to understand context, troubleshoot issues, and make judgment calls when the AI agent on the target hits problems. + +--- + +**Input**: The argument after `/opsx:porting-guide` is a change name or feature name that has already been exported via `/opsx:export-spec`. If omitted, prompt for selection. + +**Prerequisites**: An export-spec must already exist in `openspec/exports/`. This skill reads the export-spec to stay consistent with it. + +**Steps** + +1. **Locate the export artifacts** + + a. Check `openspec/exports/` for matching files: + - `-spec.md` (portable spec — required) + - `-openspec.md` (OpenSpec bundle — optional) + b. If not found, prompt with **AskUserQuestion tool**: + > "No export-spec found for ''. Available exports: [list]. Which one?" + c. Read the export-spec fully — the porting guide must not contradict it. + +2. **Read the source material** + + Same sources as export-spec, but read for **context and rationale** rather than contracts: + a. All archived change proposals, designs, and tasks in `openspec/changes/archive/` + b. All main specs in `openspec/specs/` + c. The actual implementation source files + d. Any existing export-spec and OpenSpec bundle + + Focus on extracting: + - **Decision rationale**: WHY each design choice was made + - **Rejected alternatives**: what was considered and discarded + - **Friction points**: where the source implementation hit problems + - **Implicit knowledge**: things the source developer knew but didn't document + - **Platform traps**: runtime behaviors that aren't obvious from the code + +3. **Gather target context** + + If not already captured in the export-spec, use **AskUserQuestion tool** to learn: + > "To write accurate porting notes, I need to understand the target: + > 1. What patterns does the target use for DI registration? (manual, Scrutor, Autofac?) + > 2. What's the deployment model? (Azure, on-prem, Docker?) + > 3. Are there known constraints? (network restrictions, NuGet source limits, proxy requirements?) + > 4. Who will be implementing — developer familiarity with the source stack?" + + This shapes the level of explanation and which friction points to highlight. + +4. **Generate the porting guide** + + Create a comprehensive markdown document structured for a human reader. + Use narrative prose where it aids understanding, tables for quick reference, + and checklists for actionable steps. + + ### Structure: + + ```markdown + # Porting Guide: + ## Source: | Export: + + ## How to Use This Guide + + Brief orientation: + - Relationship to the AI-targeted export-spec + - When to read this vs when to let the AI work + - How sections are organized + + ## Architecture Overview + + 2-3 paragraphs a human can read in 5 minutes to understand the full feature. + Written as narrative, not bullet points. Cover: + - What the feature does end-to-end (user perspective) + - How data flows through the system (request → processing → response) + - What external dependencies exist and why + - The "one thing you must understand" about this feature + + ## Design Decisions (Detailed) + + For EACH significant design decision: + + ### + + **What we chose:** + + **Why:** + + **What we rejected:** + - — rejected because + - — rejected because + + **When you'd revisit this:** + + **Target adaptation:** + + Decisions to always cover: + - Framework/library choices (SK over raw HTTP, SSE over WebSocket, etc.) + - Lifetime/scope decisions (singleton vs scoped vs transient) + - State management approach + - Streaming strategy + - Security decisions (sanitization, auth, CORS) + - File/folder organization + + ## Source → Target Mapping + + Side-by-side mapping table showing how source concepts translate to target: + + | Source (ChatAgent) | Target (CRC) | Notes | + |---|---|---| + | `ChatAgent.Api/` | `CRC.Server/` | CRC uses hosted model, not standalone API | + | `ChatAgent.Client/` | `CRC.Client/` | Same WASM pattern | + | `builder.Services.AddScoped()` | Check if Scrutor handles this | CRC may auto-scan | + | ... | ... | ... | + + Flag EVERY known divergence, no matter how small. The small ones cause the most debugging time. + + ## Task-by-Task Implementation Notes + + For EACH task in the OpenSpec tasks.md: + + ### : + + **Prerequisites:** What must be completed first and verified working. + + **Context:** Why this task exists and what it accomplishes in the bigger picture. + A developer who understands the "why" makes better judgment calls when adapting. + + **Step-by-step:** + 1. + 2. + 3. + + **Expected friction on target:** + - + - + + **Verify it works:** + - + - + - + + **If it breaks — diagnostic checklist:** + - Symptom: + Cause: + Fix: + - Symptom: + Cause: + Fix: + + ## Troubleshooting Reference + + Comprehensive symptom → cause → fix table for known porting issues. + Organized by category (network, DI, UI, streaming, auth, config). + + | Symptom | Likely Cause | Fix | + |---|---|---| + | 404 on /v1/chat/completions | Base URL missing `/v1` | Add `/v1` to `NlxvaPricer:LlmBaseUrl` | + | CORS 403 on SSE endpoint | CORS policy doesn't cover `text/event-stream` | Add origin to CORS policy | + | Streaming hangs, no tokens | `SetBrowserResponseStreamingEnabled` missing | Add to HttpRequestMessage | + | `EndOfStream` throws | Synchronous peek on async-only WASM stream | Use `ReadLineAsync() != null` loop | + | Markdown not rendering | MarkdownService not registered in DI | Add `AddSingleton()` | + | Tools never called by LLM | Plugin not imported on request | Move ImportPluginFromObject into action method | + | ... | ... | ... | + + Include at least 10-15 entries covering the most likely failure modes. + + ## Configuration Checklist + + Every config key the feature needs, organized as a checklist: + + - [ ] `NlxvaPricer:LlmBaseUrl` — Where: `appsettings.json` (CRC.Server) — Default: `http://localhost:8317/v1` — What happens if missing: Falls back to default, fails if proxy not running + - [ ] `NlxvaPricer:LlmModel` — Where: `appsettings.json` — Default: `claude-sonnet-4-6` + - [ ] ... (every key) + + ## Dependency & Package Notes + + For each new package: + - Package name and version constraint + - Why it's needed (one sentence) + - .NET version compatibility notes + - Known conflicts with packages the target might already have + - NuGet source: public nuget.org or internal feed? + (CRC uses internal GV Artifactory — flag if a package might not be available there) + + ## Rollback Plan + + If the feature needs to be removed: + - Which files were added (safe to delete) + - Which files were modified and what was added (revert specific sections) + - Which NuGet packages were added (remove from csproj) + - Which config keys were added (remove from appsettings.json) + ``` + +5. **Cross-check against export-spec** + + Before writing, verify: + - Every contract in the export-spec is explained in the porting guide + - Every critical pattern has a troubleshooting entry + - No contradictions between the two documents + - The porting guide adds CONTEXT, not ALTERNATIVE instructions + +6. **Write the output** + + Save to: `openspec/exports/-porting-guide.md` + Display a summary of sections generated and key highlights. + +**Guardrails** + +- **Narrative over telegraphic**: This is for humans. Use complete sentences. Explain the "why." +- **Never contradict the export-spec**: The AI spec is the source of truth for contracts and patterns. The porting guide explains and contextualizes, it doesn't override. +- **Anticipate the debugging session**: For every task, imagine the developer hitting a wall. What would they need to know? Write that down. +- **Be specific about the target**: Generic advice ("check your config") is useless. Specific advice ("CRC uses `gv_web_config.csv` for primary config but `appsettings.json` for secondary — LLM config goes in appsettings") saves hours. +- **Include the embarrassing details**: The things that took 30 minutes to figure out in the source (like the `/v1` URL requirement, or `EndOfStream` hanging) — those are the most valuable entries. +- **Flag NuGet source issues**: If the target uses an internal feed (like CRC's GV Artifactory with nuget.org disabled), flag every package that needs to come from a specific source. +- **No code blocks for code's sake**: The export-spec has the exact code. The porting guide references it ("see Critical Pattern #2 in the export-spec") rather than duplicating it. Only include code when it clarifies a specific porting concern. +- **Version-stamp the guide**: Include the date and source commit hash so the reader knows what state the guide reflects. + +**Anti-patterns** + +1. **Don't duplicate the export-spec.** The export-spec has exact contracts and code snippets. The porting guide has context, rationale, and troubleshooting. They complement each other. + +2. **Don't be abstract.** "There might be DI issues" is useless. "CRC uses Scrutor for assembly scanning. If Scrutor auto-registers ExtractionPlugin before your manual AddScoped(), you'll get a duplicate registration. Check with `builder.Services.Where(s => s.ServiceType == typeof(ExtractionPlugin))` in a breakpoint" is useful. + +3. **Don't skip the obvious.** The developer may be senior but unfamiliar with Semantic Kernel. Explain SK concepts (Kernel, plugins, FunctionChoiceBehavior) in plain language. + +4. **Don't assume the happy path.** Network restrictions, proxy requirements, package feed limitations, and auth quirks are the norm in enterprise environments. Address them. + +ARGUMENTS: $ARGUMENTS diff --git a/openspec/exports/nlxva-pricer-openspec.md b/openspec/exports/nlxva-pricer-openspec.md new file mode 100644 index 0000000..0658389 --- /dev/null +++ b/openspec/exports/nlxva-pricer-openspec.md @@ -0,0 +1,196 @@ +# 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 OpenAI-compatible proxy +- 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: 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: 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+. + +## 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` to CRC.Server. Add `Markdig` 1.1.1 to CRC.Client (if not already present). 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: `AddOpenAIChatCompletion()` + `AddKernel()`. Base URL MUST include `/v1`. Config from `NlxvaPricer:*` keys in appsettings.json. See Critical Pattern #2. + +- [ ] **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 `")` returns empty string (script stripped) + +--- + +### T9: Add NlxvaPricerApiClient + +**Prerequisites:** T2 (DTOs for request/response types). + +**Context:** This is the client-side service that talks to the NlxvaPricerController. The critical pattern is SSE streaming in Blazor WASM — this is where the most subtle bugs occur. + +**Step-by-step:** +1. Create `CRC.Client/Services/NlxvaPricerApiClient.cs` +2. Register with `AddHttpClient()` pointing to CRC.Server's URL +3. Implement streaming methods using the exact pattern from export-spec Critical Pattern #1 + +**Expected friction on target:** +- **SetBrowserResponseStreamingEnabled**: This extension method is from `Microsoft.AspNetCore.Components.WebAssembly.Http`. If CRC.Client doesn't already reference this namespace, add a `@using` or `using` directive. +- **Blazor WASM HttpClient behavior**: In WASM, HttpClient is backed by the browser's Fetch API. The `SetBrowserResponseStreamingEnabled(true)` flag is essential — without it, the browser buffers the entire response and streaming won't work (the user sees nothing until the full response completes, then everything at once). + +**Verify it works:** +- In browser dev tools (Network tab): the request to `/api/nlxva-pricer/chat` should show as `EventStream` type with chunks arriving over time, not a single response. + +**If it breaks — diagnostic checklist:** +- Symptom: No tokens appear during streaming; entire response appears at once after completion + Cause: `SetBrowserResponseStreamingEnabled(true)` missing + Fix: Add to the HttpRequestMessage before sending +- Symptom: `NotSupportedException: Synchronous operations are not allowed` + Cause: Using `reader.EndOfStream` (performs sync read) + Fix: Replace with `while ((line = await reader.ReadLineAsync()) != null)` loop +- Symptom: `yield return` inside `try` block causes compiler error + Cause: C# language restriction — `yield` cannot appear inside `try-catch` + Fix: Parse into local variables inside `try`, yield outside (see Critical Pattern #7 in export-spec) + +--- + +### T10: Add file-drop.js + +**Prerequisites:** None (standalone JS file). + +**Context:** Blazor WASM's built-in drag-and-drop support is limited. This JS file handles browser drag/drop events and calls back to .NET. + +**Step-by-step:** +1. Create `CRC.Client/wwwroot/js/file-drop.js` +2. Add `` to CRC.Client's `index.html` +3. Place the script tag BEFORE the Blazor framework script (`_framework/blazor.webassembly.js`) + +**Expected friction on target:** +- **Script loading order**: If placed after the Blazor script, `window.fileDrop` may not be defined when the component tries to call it. Place before. +- **Existing JS in CRC**: If CRC already bundles JS, consider whether to integrate with their bundling approach or keep as a standalone file. + +**Verify it works:** +- Browser console: `window.fileDrop` should be defined (type `fileDrop` in console) + +--- + +### T11: Add NlxvaPricer.razor page + +**Prerequisites:** T8 (MarkdownService), T9 (ApiClient), T10 (file-drop.js). + +**Context:** This is the main UI component — the largest single file. It brings together chat, streaming, markdown rendering, email upload, extraction mode, and prompt settings. + +**Step-by-step:** +1. Create `CRC.Client/Pages/NlxvaPricer.razor` +2. Route: `@page "/nlxva-pricer"` +3. Page title: `NL XVA Pricer` +4. Inject: `IJSRuntime`, `NlxvaPricerApiClient`, `MarkdownService` +5. Build the component following the export-spec structure + +**Expected friction on target:** +- **MudBlazor version differences**: If CRC uses MudBlazor 6.x or 7.x, some component APIs differ (see MudBlazor Version Warning in Source → Target Mapping). +- **CSS calc height**: MUST use `calc(100vh - 64px)` for CRC's regular-height AppBar. The source uses `48px` (Dense AppBar). Getting this wrong means the chat area either overflows below the viewport or leaves a gap. +- **MudChip generic parameter**: In MudBlazor 9.x, `MudChip` requires `T="string"`. In older versions, it doesn't. Check CRC's version. +- **KeepPanelsAlive**: If CRC's MudBlazor version doesn't support this prop, tab content will reset when switching tabs. Workaround: store prompt text and settings in `@code` fields (which we already do — the issue is that the MudTextField would lose focus/cursor position). + +**Verify it works:** +- Navigate to `/nlxva-pricer` +- Type a message and press Enter → should see streaming tokens +- Switch to System Prompt tab, edit prompt, switch back → prompt should be preserved +- Drop an `.html` email file → should enter extraction mode +- Click New Chat → should reset everything + +--- + +### T12: Add NlxvaPricer.razor.css + +**Prerequisites:** T11 (the page must exist for scoped CSS to apply). + +**Context:** Blazor CSS isolation scopes these styles to the NlxvaPricer component only. The `::deep` combinator is needed for styles targeting MudBlazor child markup. + +**Key adaptation:** +- Change `calc(100vh - 48px)` to `calc(100vh - 64px)` for CRC's AppBar height +- If CRC uses `100dvh` elsewhere, prefer that over `100vh` + +**Verify it works:** +- The chat area should fill the viewport between the AppBar and the bottom of the page +- Messages should scroll within the message-list area +- No horizontal overflow on code blocks + +--- + +### T13: Add NavMenu link + +**Prerequisites:** T11 (page must exist to navigate to). + +**Context:** Adding a single MudNavLink to CRC's existing NavMenu. + +**Step-by-step:** +1. Find CRC's NavMenu component (likely `CRC.Client/Shared/NavMenu.razor` or `CRC.Client/Layout/NavMenu.razor`) +2. Add the MudNavLink at the end of the existing list +3. Do NOT rearrange or modify existing links + +**Expected friction on target:** +- **NavMenu structure**: CRC may use a different NavMenu component structure than what we have in ChatAgent. Look at how existing links are defined and follow the same pattern. + +**Verify it works:** +- The "NL XVA Pricer" link appears in the sidebar +- Clicking it navigates to `/nlxva-pricer` without a full page reload + +--- + +### T14: Configuration + +**Prerequisites:** All server tasks complete. + +**Step-by-step checklist:** + +- [ ] `NlxvaPricer:LlmBaseUrl` in CRC.Server `appsettings.json` — default `http://localhost:8317/v1` +- [ ] `NlxvaPricer:LlmModel` in CRC.Server `appsettings.json` — default `claude-sonnet-4-6` +- [ ] `NlxvaPricer:LlmApiKey` in CRC.Server `appsettings.json` — default `not-needed` +- [ ] `NlxvaPricer:FewShotPath` in CRC.Server `appsettings.json` — default `examples/extraction` +- [ ] `ExternalApis:CounterpartyBaseUrl` — default `http://localhost:5000/api/counterparty` (or use CRC's existing) +- [ ] `ExternalApis:TradeBaseUrl` — default `http://localhost:5000/api/trade` (or use CRC's existing) +- [ ] `ExternalApis:CurrencyBaseUrl` — default `http://localhost:5000/api/currency` (or use CRC's existing) +- [ ] CRC.Client `appsettings.json` or `wwwroot/appsettings.json` — API base URL +- [ ] CORS policy in CRC.Server — verify CRC.Client origin is allowed +- [ ] `examples/` folder exists at configured path with `instruction-template.txt` and `few-shot/` subdirectories + +--- + +### T15: Smoke test + +**Full verification sequence:** + +1. `dotnet build --configuration release CRC.sln` — 0 errors, 0 new warnings +2. Start CLIProxyAPI on target machine +3. Start CRC.Server +4. Navigate to CRC.Client in browser +5. Verify "NL XVA Pricer" appears in sidebar +6. Click it → should navigate to `/nlxva-pricer` +7. Type "Hello" → should see streaming response +8. Switch to System Prompt tab → should see default prompt +9. Switch to Model Settings tab → should see Temperature/TopP/MaxTokens fields +10. Switch back to Chat → conversation should still be there (KeepPanelsAlive) +11. Drop an example email .html → should enter Extraction Mode, see streaming extraction +12. If extraction produces counterparty disambiguation → respond with a number → should route to extract endpoint +13. Click New Chat → everything resets, Extraction Mode indicator gone + +--- + +## Troubleshooting Reference + +| # | Symptom | Likely Cause | Fix | +|---|---|---|---| +| 1 | 404 on `/v1/chat/completions` | Base URL missing `/v1` suffix | Set `NlxvaPricer:LlmBaseUrl` to `http://localhost:8317/v1` | +| 2 | CORS 403 in browser console | CORS policy doesn't cover CRC.Client origin or `text/event-stream` | Add CRC.Client origin with `AllowAnyHeader()` in CORS config | +| 3 | No streaming — entire response at once | `SetBrowserResponseStreamingEnabled(true)` missing on client | Add to HttpRequestMessage before SendAsync | +| 4 | `NotSupportedException: Synchronous operations` | Using `reader.EndOfStream` in WASM | Replace with `while ((line = await ReadLineAsync()) != null)` | +| 5 | LLM never calls validation tools | Plugin not imported, or `FunctionChoiceBehavior.Auto()` not set | Import plugin per-request in controller action; set Auto() in settings | +| 6 | `InvalidOperationException: Unable to resolve ExtractionPlugin` | Not registered in DI, or lifetime mismatch | Add `AddScoped()` to DI; verify dependencies | +| 7 | Markdown not rendering (raw text) | MarkdownService not registered or not injected | Add `AddSingleton()` to client DI | +| 8 | XSS in rendered content | Sanitizer not applied, or using raw `MarkupString` without sanitization | Ensure ConvertToHtml is called (includes sanitization) before MarkupString | +| 9 | CSS overflow — chat extends below viewport | Wrong AppBar height in calc (48px vs 64px) | Change `calc(100vh - 48px)` to `calc(100vh - 64px)` | +| 10 | Tab content resets on switch | MudBlazor version missing `KeepPanelsAlive` | Upgrade MudBlazor or use state fields (already done in code pattern) | +| 11 | `FileNotFoundException` for instruction-template.txt | Examples folder not at ContentRootPath | Log ContentRootPath; verify examples location; update FewShotPath config | +| 12 | Empty few-shot examples (only system message) | Subdirectory structure wrong | Verify `examples/extraction/few-shot/01/input.html` exists | +| 13 | `NuGet restore error` for SemanticKernel | Package not in GV Artifactory feed | Request mirroring or temporary nuget.org source | +| 14 | `HttpRequestException: Connection refused` | CLIProxyAPI not running | Start proxy; verify with `curl http://localhost:8317/v1/models` | +| 15 | Drag-drop file not triggering extraction | `file-drop.js` not loaded | Check ` +``` + +### CRC.Server CORS (if not already allowing the client origin) + +Ensure the CORS policy allows the CRC.Client origin for the new endpoints. + +### Examples folder + +Copy the `examples/extraction/` folder to the CRC.Server project root: +``` +examples/extraction/ +├── instruction-template.txt +└── few-shot/ + ├── 01/ + │ ├── input.html + │ └── output.json + ├── 02/ + │ ├── input.html + │ └── output.json + └── 03/ + ├── input.html + └── output.json +``` + +## Behavior + +- **Extraction mode routing**: When an email is uploaded, `_isExtractionMode = true`. All subsequent text messages route to `/extract` (not `/chat`) until "New Chat" resets +- **Follow-up disambiguation**: The extraction endpoint receives full conversation history (email + all prior exchanges) so the agent has context for disambiguation +- **Upload message**: File upload adds a user message `[Uploaded: filename.html]` to the chat before streaming the extraction response +- **File validation**: Only `.html` files accepted (both drag-drop and file picker). Others show MudAlert warning +- **Streaming guard**: Input field, send button, upload button, and drop zone all disabled during streaming +- **Multi-turn context**: General chat sends full conversation history with every request +- **System prompt**: Only used for general chat, NOT for extraction (extraction uses fixed instruction template) +- **Model settings**: Only used for general chat, NOT for extraction +- **Settings persistence**: In-memory only (lost on page refresh) — acceptable for a debugging/iteration tool +- **DotNetObjectReference disposal**: Chat page implements IDisposable to dispose the JS interop reference + +## Few-Shot Instruction Template + +The instruction template defines the extraction task. Content: + +``` +You are a trade data extraction agent. Your task is to extract structured trade data +from sales emails (typically CVA pricing requests) and return the result as JSON. + +## Output Schema +Return a JSON object with an "items" array. Each item has: +- valuedate (string): dd/MM/yyyy format +- counterparty (string): full legal name from email +- trade_id (integer): Murex trade ID +- display_ccy (string): ISO currency code (£→GBP, $→USD, €→EUR) +- pv (number): plain number, no formatting +- breakclause (string): "Y" or "N" (default "N") + +legal_entity is NOT included — populated later via lookup tool. + +## Mapping Rules +1. FLATTEN: Each leg with unique Murex ID → separate item +2. DATE: Parse from context (e.g., "OB 27/11/2025" → "27/11/2025") +3. COUNTERPARTY: Full legal name exactly as written +4. CURRENCY: From PV column header (£→GBP, $→USD, €→EUR) +5. PV: Strip commas/symbols, plain number +6. BREAKCLAUSE: Default "N", only "Y" if explicitly mentioned + +## After Extraction +Use tools: lookup_counterparty, validate_trade, validate_currency, validate_schema. +If multiple candidates, present numbered list and ask user to select. +``` + +--- + +## Compression Stats + +- Source code: ~3,200 lines across 25+ files +- This spec: ~350 lines +- Compression ratio: ~9:1 +- Estimated typing: ~12,000 characters (vs ~110,000 for full code)