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) <noreply@anthropic.com>
This commit is contained in:
512
openspec/exports/nlxva-pricer-spec.md
Normal file
512
openspec/exports/nlxva-pricer-spec.md
Normal file
@@ -0,0 +1,512 @@
|
||||
# Feature: Natural Language XVA Pricer
|
||||
## Target: CRC (Blazor WASM Hosted / ASP.NET Core / MudBlazor / .NET 8.0)
|
||||
## Source: ChatAgent — cumulative export of all 12 changes
|
||||
## Includes: chat-ui, chat-streaming, semantic-kernel, multi-turn, rich-text, sidebar-nav, prompt-settings, extraction-schema, extraction-tools, few-shot-prompting, extraction-endpoint, email-upload
|
||||
## Skipped: migrate-claude-md-to-openspec (documentation only), add-test-coverage (adapt to CRC test conventions separately)
|
||||
|
||||
---
|
||||
|
||||
## Integration Rule
|
||||
|
||||
This feature is a GUEST in CRC. Existing code, patterns, and conventions take absolute precedence.
|
||||
|
||||
- **DO NOT** modify existing files, components, layouts, services, routing, or DI registrations in CRC
|
||||
- **DO NOT** replace existing patterns (e.g., if CRC uses a different HttpClient pattern, use theirs)
|
||||
- **DO** add new files, new nav links, new routes, new DI registrations
|
||||
- **DO** conform to CRC naming conventions: `E`-prefix enums, `I`-prefix interfaces, `*Dto`/`*Request`/`*Response` DTOs, PascalCase constants, `{Subject}Test` test classes
|
||||
- **DO** use CRC.Shared for DTOs (not a new shared project)
|
||||
- If a task conflicts with existing CRC code, **STOP and ask the user**
|
||||
- If CRC already has an equivalent service (HttpClient wrapper, markdown renderer), **use the existing one**
|
||||
|
||||
### Adapt-to-target notes
|
||||
- CRC uses `CRC.Server` (not a standalone API project) — add controller and services there
|
||||
- CRC uses `CRC.Client` — add pages, layout changes, client services there
|
||||
- CRC uses `CRC.Shared` — add DTOs there
|
||||
- CRC uses Scrutor for DI assembly scanning — register new services compatibly
|
||||
- CRC uses Fluxor for client state — this feature uses local component state (no Fluxor needed), which is fine for an isolated page
|
||||
- CRC uses Serilog — use `ILogger<T>` via DI (Serilog handles the sink)
|
||||
- CRC uses Azure AD auth in prod, DevAuth in dev — add `[Authorize]` if CRC controllers require it
|
||||
- CRC uses `gv_web_config.csv` as primary config — put LLM config in `appsettings.json` (secondary config) where CRC already stores Serilog/DevAuth settings
|
||||
- CRC AppBar is regular height (64px), not Dense (48px) — adjust CSS calc accordingly
|
||||
|
||||
## Target Layout
|
||||
|
||||
```
|
||||
+------------------------------------------------------------------+
|
||||
| CRC AppBar (64px, blue, Elevation 1) |
|
||||
| [=] CRC 0.0.0 APR-CRC-PROD-LDN-DEV |
|
||||
+------+-----------------------------------------------------------+
|
||||
| Drawer| MudMainContent |
|
||||
| Home | |
|
||||
| Pricer| (routed page content) |
|
||||
| Mkt | |
|
||||
| XVA | |
|
||||
| Sales | |
|
||||
|>NLPric| <-- NEW: "NL XVA Pricer" nav item, route /nlxva-pricer |
|
||||
| | |
|
||||
+------+-----------------------------------------------------------+
|
||||
```
|
||||
|
||||
- Feature name: **NL XVA Pricer** (short for Natural Language XVA Pricer)
|
||||
- Route: `/nlxva-pricer`
|
||||
- Navigation: new MudNavLink in the existing NavMenu component
|
||||
- Icon: `Icons.Material.Filled.SmartToy`
|
||||
- AppBar height: 64px (CRC uses regular, NOT Dense)
|
||||
- CSS viewport calc: `calc(100vh - 64px)` (NOT 48px)
|
||||
|
||||
## Packages
|
||||
|
||||
Add to `CRC.Server`:
|
||||
- `Microsoft.SemanticKernel` (latest stable, >=1.x)
|
||||
- `Markdig` 1.1.1 (if CRC.Client doesn't already have it — check first)
|
||||
|
||||
No new packages for CRC.Client or CRC.Shared (MudBlazor already present).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CRC.Client (WASM)
|
||||
|
|
||||
| HTTP REST (SSE streaming)
|
||||
|
|
||||
CRC.Server (ASP.NET Core)
|
||||
├── NlxvaPricerController
|
||||
│ ├── POST /api/nlxva-pricer/chat (general chat)
|
||||
│ └── POST /api/nlxva-pricer/extract (email extraction)
|
||||
│ Uses: Semantic Kernel → CLIProxyAPI (OpenAI-compatible proxy)
|
||||
│ Uses: ExtractionPlugin (tool calling)
|
||||
│ Uses: FewShotService (example loading)
|
||||
├── Services/
|
||||
│ ├── FewShotService (singleton, loads examples at startup)
|
||||
│ ├── CounterpartyApiClient (typed HttpClient)
|
||||
│ ├── TradeApiClient (typed HttpClient)
|
||||
│ └── CurrencyApiClient (typed HttpClient)
|
||||
├── Plugins/
|
||||
│ └── ExtractionPlugin ([KernelFunction] tools)
|
||||
├── CRC.Shared (DTOs)
|
||||
└── CRC.Component (if reusable Blazor components needed)
|
||||
```
|
||||
|
||||
Two endpoints, same SSE streaming contract. General chat supports system prompt + model settings.
|
||||
Extraction uses few-shot prefix (not user system prompt) and extraction-specific tools.
|
||||
|
||||
## Components
|
||||
|
||||
### Page: `NlxvaPricer.razor` → `CRC.Client/Pages/NlxvaPricer.razor`
|
||||
- Route: `@page "/nlxva-pricer"`
|
||||
- MudTabs with 3 panels: Chat, System Prompt, Model Settings (KeepPanelsAlive=true)
|
||||
- Chat panel: message list (scrollable), input area (text field + send + upload button), drag-drop zone
|
||||
- Extraction mode: tracked by `_isExtractionMode` bool; routes subsequent messages to extract endpoint
|
||||
- Streaming: consumes `IAsyncEnumerable<string>`, appends token-by-token to assistant message
|
||||
- Markdown rendering: assistant messages rendered via MarkdownService + MarkupString
|
||||
- HTML render cache: `Dictionary<ChatMessage, string>` avoids re-running Markdig on completed messages
|
||||
- JS interop: auto-scroll, drag-and-drop file handling via `file-drop.js`
|
||||
|
||||
### Client service: `NlxvaPricerApiClient` → `CRC.Client/Services/NlxvaPricerApiClient.cs`
|
||||
- Typed HttpClient wrapper
|
||||
- `SendChatStreamingAsync(NlxvaChatRequest)` → POST /api/nlxva-pricer/chat, returns `IAsyncEnumerable<string>`
|
||||
- `SendExtractionStreamingAsync(NlxvaExtractionRequest)` → POST /api/nlxva-pricer/extract, returns `IAsyncEnumerable<string>`
|
||||
- SSE parsing: read line-by-line, extract `data: {"text":"..."}` events, yield text deltas, stop at `[DONE]`
|
||||
|
||||
### Client service: `MarkdownService` → `CRC.Client/Services/MarkdownService.cs`
|
||||
- Markdig pipeline with `UseAdvancedExtensions()`
|
||||
- HTML sanitization via tag/attribute allowlist (p, h1-h6, strong, em, code, pre, ul, ol, li, a[href], table/thead/tbody/tr/th/td, br, blockquote)
|
||||
- Strips `<script>`, `<style>` blocks entirely, strips event handler attributes
|
||||
- Singleton registration
|
||||
|
||||
### Controller: `NlxvaPricerController` → `CRC.Server/Controllers/NlxvaPricerController.cs`
|
||||
- `[Route("api/nlxva-pricer")]`
|
||||
- `POST /` (chat): builds SK ChatHistory from messages + optional system prompt, streams SSE
|
||||
- `POST /extract`: builds ChatHistory from FewShotService prefix + email, streams SSE
|
||||
- Both endpoints: import ExtractionPlugin, enable `FunctionChoiceBehavior.Auto()`
|
||||
- SSE format: `data: {"text":"..."}\n\n` per token, `data: [DONE]\n\n` at end, `data: {"error":"..."}\n\n` on failure
|
||||
|
||||
### Plugin: `ExtractionPlugin` → `CRC.Server/Plugins/ExtractionPlugin.cs`
|
||||
- 4 `[KernelFunction]` methods:
|
||||
- `lookup_counterparty(string name)` → calls CounterpartyApiClient, returns JSON ValidationResult
|
||||
- `validate_trade(long tradeId)` → calls TradeApiClient
|
||||
- `validate_currency(string currencyCode)` → calls CurrencyApiClient
|
||||
- `validate_schema(string extractionResultJson)` → local JSON validation against TradeItem schema
|
||||
- All return serialized `ValidationResult` JSON (so LLM can reason about it)
|
||||
- HTTP errors caught and returned as structured messages (not thrown)
|
||||
|
||||
### Service: `FewShotService` → `CRC.Server/Services/FewShotService.cs`
|
||||
- Loads instruction template + few-shot examples from disk at startup
|
||||
- Caches a `ChatHistory` prefix (system message + alternating user/assistant example turns)
|
||||
- `CloneWithEmail(string emailHtml)` → clones prefix + appends email as final user message
|
||||
- `CloneWithEmailAndMessages(string emailHtml, List<NlxvaChatMessage> messages)` → for follow-ups
|
||||
- Singleton lifetime
|
||||
|
||||
### API Clients: `CounterpartyApiClient`, `TradeApiClient`, `CurrencyApiClient`
|
||||
- Each: typed HttpClient with single async method wrapping an external API call
|
||||
- Registered via `AddHttpClient<T>()` with base URL from appsettings.json
|
||||
- CounterpartyApiClient.LookupAsync(name) → `GET lookup?name={name}` → `List<CandidateMatch>`
|
||||
- TradeApiClient.ValidateAsync(tradeId) → `GET validate/{tradeId}` → `TradeValidationResponse`
|
||||
- CurrencyApiClient.ValidateAsync(code) → `GET validate/{code}` → `CurrencyValidationResponse`
|
||||
|
||||
### JS: `file-drop.js` → `CRC.Client/wwwroot/js/file-drop.js`
|
||||
- Registers dragover/dragenter/dragleave/drop handlers on a CSS-selector target
|
||||
- Reads dropped file as text via FileReader
|
||||
- Calls back to .NET via `DotNetObjectReference.invokeMethodAsync`
|
||||
|
||||
## Contracts
|
||||
|
||||
### DTOs (all in CRC.Shared namespace, adapt naming to CRC conventions)
|
||||
|
||||
```csharp
|
||||
// NlxvaChatMessage.cs
|
||||
public class NlxvaChatMessage
|
||||
{
|
||||
public string Role { get; set; } = string.Empty; // "user" | "assistant"
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
|
||||
// NlxvaChatRequest.cs — POST /api/nlxva-pricer/chat
|
||||
public class NlxvaChatRequest
|
||||
{
|
||||
public List<NlxvaChatMessage> Messages { get; set; } = new();
|
||||
public string? SystemPrompt { get; set; }
|
||||
public NlxvaModelSettings? Settings { get; set; }
|
||||
}
|
||||
|
||||
// NlxvaModelSettings.cs
|
||||
public class NlxvaModelSettings
|
||||
{
|
||||
public double? Temperature { get; set; } // 0.0–2.0
|
||||
public double? TopP { get; set; } // 0.0–1.0
|
||||
public int? MaxTokens { get; set; } // 1–4096
|
||||
}
|
||||
|
||||
// NlxvaExtractionRequest.cs — POST /api/nlxva-pricer/extract
|
||||
public class NlxvaExtractionRequest
|
||||
{
|
||||
public string EmailHtml { get; set; } = string.Empty;
|
||||
public List<NlxvaChatMessage> Messages { get; set; } = new();
|
||||
}
|
||||
|
||||
// TradeItem.cs — snake_case JSON for downstream systems
|
||||
public class TradeItem
|
||||
{
|
||||
[JsonPropertyName("valuedate")] public string? Valuedate { get; set; }
|
||||
[JsonPropertyName("counterparty")] public string? Counterparty { get; set; }
|
||||
[JsonPropertyName("legal_entity")] public string? LegalEntity { get; set; }
|
||||
[JsonPropertyName("trade_id")] public long TradeId { get; set; }
|
||||
[JsonPropertyName("display_ccy")] public string? DisplayCcy { get; set; }
|
||||
[JsonPropertyName("pv")] public double Pv { get; set; }
|
||||
[JsonPropertyName("breakclause")] public string? Breakclause { get; set; }
|
||||
}
|
||||
|
||||
// NlxvaExtractionResult.cs
|
||||
public class NlxvaExtractionResult
|
||||
{
|
||||
[JsonPropertyName("items")]
|
||||
public List<TradeItem> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
// NlxvaValidationResult.cs
|
||||
public class NlxvaValidationResult
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public List<string> Errors { get; set; } = new();
|
||||
public List<NlxvaCandidateMatch>? Candidates { get; set; }
|
||||
}
|
||||
|
||||
public class NlxvaCandidateMatch
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string LegalEntity { get; set; } = string.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
### SSE Wire Format
|
||||
|
||||
```
|
||||
data: {"text":"token here"}\n\n ← per token
|
||||
data: [DONE]\n\n ← stream complete
|
||||
data: {"error":"message"}\n\n ← on failure (followed by [DONE])
|
||||
```
|
||||
|
||||
### Config keys (appsettings.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"NlxvaPricer": {
|
||||
"LlmBaseUrl": "http://localhost:8317/v1",
|
||||
"LlmModel": "claude-sonnet-4-6",
|
||||
"LlmApiKey": "not-needed",
|
||||
"FewShotPath": "examples/extraction"
|
||||
},
|
||||
"ExternalApis": {
|
||||
"CounterpartyBaseUrl": "http://localhost:5000/api/counterparty",
|
||||
"TradeBaseUrl": "http://localhost:5000/api/trade",
|
||||
"CurrencyBaseUrl": "http://localhost:5000/api/currency"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Critical Patterns
|
||||
|
||||
### 1. SSE streaming in Blazor WASM — DO NOT use `reader.EndOfStream`
|
||||
|
||||
**Why:** `EndOfStream` performs a synchronous peek read. Blazor WASM's async streaming pipeline
|
||||
does not support synchronous reads — it will hang or throw.
|
||||
|
||||
**Copy this pattern:**
|
||||
```csharp
|
||||
httpRequest.SetBrowserResponseStreamingEnabled(true);
|
||||
|
||||
using var response = await _httpClient.SendAsync(
|
||||
httpRequest, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null) // ← NOT EndOfStream
|
||||
{
|
||||
if (!line.StartsWith("data: ")) continue;
|
||||
var data = line.Substring(6);
|
||||
if (data == "[DONE]") yield break;
|
||||
// parse {"text":"..."} and yield
|
||||
}
|
||||
```
|
||||
|
||||
`SetBrowserResponseStreamingEnabled(true)` is a Blazor WASM extension that tells the browser Fetch API
|
||||
to expose the response as a ReadableStream. Without it, the browser buffers the entire response.
|
||||
|
||||
### 2. Semantic Kernel base URL must include `/v1`
|
||||
|
||||
**Why:** The OpenAI SDK appends `chat/completions` directly to the base URL.
|
||||
Without `/v1`, requests hit `/chat/completions` instead of `/v1/chat/completions` → 404.
|
||||
|
||||
```csharp
|
||||
builder.Services.AddOpenAIChatCompletion(
|
||||
modelId: model,
|
||||
endpoint: new Uri("http://localhost:8317/v1"), // ← MUST include /v1
|
||||
apiKey: "not-needed");
|
||||
```
|
||||
|
||||
### 3. Layout height depends on AppBar height
|
||||
|
||||
**Why:** CRC uses a regular AppBar (64px), not Dense (48px). Magic CSS values must match.
|
||||
|
||||
```css
|
||||
::deep .tab-container {
|
||||
height: calc(100vh - 64px); /* 64px = CRC regular AppBar height */
|
||||
}
|
||||
```
|
||||
|
||||
If CRC uses `100dvh` elsewhere, prefer that over `100vh` for mobile viewport correctness.
|
||||
|
||||
### 4. Markdown render caching during streaming
|
||||
|
||||
**Why:** Without caching, every `StateHasChanged()` during streaming re-runs Markdig on ALL messages,
|
||||
causing visible lag as conversation grows. Only the streaming message should re-render.
|
||||
|
||||
```csharp
|
||||
private readonly Dictionary<NlxvaChatMessage, string> _renderedHtmlCache = new();
|
||||
|
||||
private string GetRenderedHtml(NlxvaChatMessage message)
|
||||
{
|
||||
if (_renderedHtmlCache.TryGetValue(message, out var cached))
|
||||
return cached;
|
||||
return Markdown.ConvertToHtml(message.Content);
|
||||
}
|
||||
|
||||
// In finally block after streaming completes:
|
||||
_renderedHtmlCache[assistantMessage] = Markdown.ConvertToHtml(assistantMessage.Content);
|
||||
```
|
||||
|
||||
### 5. ExtractionPlugin tool results must be serialized JSON strings
|
||||
|
||||
**Why:** SK passes the return value as a string to the LLM. The LLM needs structured JSON
|
||||
to reason about validation results, error messages, and candidate lists.
|
||||
|
||||
```csharp
|
||||
[KernelFunction("lookup_counterparty")]
|
||||
[Description("Looks up counterparty candidates by name...")]
|
||||
public async Task<string> LookupCounterparty(string name)
|
||||
{
|
||||
var result = new NlxvaValidationResult();
|
||||
// ... populate result ...
|
||||
return JsonSerializer.Serialize(result); // ← return JSON string, not object
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Per-request plugin import (not global)
|
||||
|
||||
**Why:** Plugins that depend on scoped services (typed HttpClients) must be imported per-request,
|
||||
not registered globally on the Kernel at startup.
|
||||
|
||||
```csharp
|
||||
var extractionPlugin = HttpContext.RequestServices.GetRequiredService<ExtractionPlugin>();
|
||||
_kernel.ImportPluginFromObject(extractionPlugin, "Extraction");
|
||||
```
|
||||
|
||||
### 7. C# yield cannot appear inside try-catch
|
||||
|
||||
**Why:** Language restriction. SSE parsing needs to parse JSON (can throw) and yield (can't be in try).
|
||||
Solution: parse into local variables first, yield outside try block.
|
||||
|
||||
```csharp
|
||||
string? parsedText = null;
|
||||
string? parsedError = null;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(data);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("error", out var err))
|
||||
parsedError = err.GetString();
|
||||
else if (root.TryGetProperty("text", out var txt))
|
||||
parsedText = txt.GetString();
|
||||
}
|
||||
catch (JsonException) { /* skip malformed */ }
|
||||
|
||||
if (parsedError != null)
|
||||
throw new HttpRequestException($"API error: {parsedError}");
|
||||
if (!string.IsNullOrEmpty(parsedText))
|
||||
yield return parsedText;
|
||||
```
|
||||
|
||||
## Wiring
|
||||
|
||||
### CRC.Server DI registration order (add to existing Program.cs / Startup.cs)
|
||||
|
||||
```csharp
|
||||
// 1. Semantic Kernel — OpenAI-compatible connector
|
||||
var llmBaseUrl = builder.Configuration["NlxvaPricer:LlmBaseUrl"] ?? "http://localhost:8317/v1";
|
||||
var llmModel = builder.Configuration["NlxvaPricer:LlmModel"] ?? "claude-sonnet-4-6";
|
||||
builder.Services.AddOpenAIChatCompletion(
|
||||
modelId: llmModel,
|
||||
endpoint: new Uri(llmBaseUrl),
|
||||
apiKey: builder.Configuration["NlxvaPricer:LlmApiKey"] ?? "not-needed");
|
||||
builder.Services.AddKernel();
|
||||
|
||||
// 2. External API typed HttpClients
|
||||
builder.Services.AddHttpClient<CounterpartyApiClient>(c =>
|
||||
c.BaseAddress = new Uri(builder.Configuration["ExternalApis:CounterpartyBaseUrl"]
|
||||
?? "http://localhost:5000/api/counterparty"));
|
||||
builder.Services.AddHttpClient<TradeApiClient>(c =>
|
||||
c.BaseAddress = new Uri(builder.Configuration["ExternalApis:TradeBaseUrl"]
|
||||
?? "http://localhost:5000/api/trade"));
|
||||
builder.Services.AddHttpClient<CurrencyApiClient>(c =>
|
||||
c.BaseAddress = new Uri(builder.Configuration["ExternalApis:CurrencyBaseUrl"]
|
||||
?? "http://localhost:5000/api/currency"));
|
||||
|
||||
// 3. FewShotService (singleton — loads examples once at startup)
|
||||
var fewShotPath = builder.Configuration["NlxvaPricer:FewShotPath"] ?? "examples/extraction";
|
||||
var fewShotAbsPath = Path.IsPathRooted(fewShotPath)
|
||||
? fewShotPath
|
||||
: Path.Combine(builder.Environment.ContentRootPath, fewShotPath);
|
||||
builder.Services.AddSingleton(new FewShotService(fewShotAbsPath));
|
||||
|
||||
// 4. ExtractionPlugin (scoped — depends on scoped HttpClients)
|
||||
builder.Services.AddScoped<ExtractionPlugin>();
|
||||
```
|
||||
|
||||
### CRC.Client DI registration (add to existing Program.cs)
|
||||
|
||||
```csharp
|
||||
// Typed HttpClient for the NL XVA Pricer API
|
||||
builder.Services.AddHttpClient<NlxvaPricerApiClient>(c =>
|
||||
c.BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"]
|
||||
?? "https://localhost:7100/"));
|
||||
|
||||
// Markdown rendering (singleton — thread-safe, reusable)
|
||||
builder.Services.AddSingleton<MarkdownService>();
|
||||
```
|
||||
|
||||
### CRC.Client NavMenu (add new MudNavLink)
|
||||
|
||||
```razor
|
||||
<MudNavLink Href="/nlxva-pricer"
|
||||
Icon="@Icons.Material.Filled.SmartToy"
|
||||
Match="NavLinkMatch.All">
|
||||
NL XVA Pricer
|
||||
</MudNavLink>
|
||||
```
|
||||
|
||||
### CRC.Client index.html (add JS reference)
|
||||
|
||||
```html
|
||||
<script src="js/file-drop.js"></script>
|
||||
```
|
||||
|
||||
### 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)
|
||||
Reference in New Issue
Block a user