Files
AgenticCode/openspec/changes/archive/2026-04-06-update-extraction-schema/design.md
local 5b027eb0db feat: add extraction schema, sidebar nav, few-shot prompting, and prompt settings
Overhaul extraction pipeline with new TradeItem model, conversation flow,
and dedicated extraction endpoint. Add sidebar navigation with NavMenu
component and landing page. Introduce few-shot prompting service and
tests. Add prompt settings and email upload specs. Update OpenSpec
tooling with improved export-spec and extract-feature commands. Archive
completed changes and export full specs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:39:23 +01:00

5.2 KiB

Context

The extraction pipeline currently uses a placeholder schema (ExtractedFields with Client, Project, Hours, Rate, Currency, Date) and a single in-process validation method. The real domain is CVA trade extraction from sales emails: HTML emails containing swap/leg tables need to be parsed into an array of TradeItem objects. Validation and enrichment require calling 3-5 existing external APIs (counterparty lookup, trade validation, etc.), some of which return multiple candidates requiring user disambiguation.

The ExtractionPlugin is already registered with Semantic Kernel and auto-invoked via FunctionChoiceBehavior.Auto(). The tool calling loop and SSE streaming infrastructure are in place.

Goals / Non-Goals

Goals:

  • Replace placeholder models with the real TradeItem schema
  • Replace single validation method with multiple external API tool wrappers
  • Support disambiguation workflow where tool results return candidate lists
  • Keep the external API integration configurable and testable

Non-Goals:

  • Few-shot prompting infrastructure (separate change: few-shot-prompt-infrastructure)
  • Email upload/drag-drop UX (separate change: email-upload-ux)
  • Building the external APIs themselves (they already exist)
  • Changing the SSE streaming contract or client-side rendering

Decisions

1. Schema structure: wrapper with items array

Decision: Use ExtractionResult { List<TradeItem> Items } rather than returning a flat TradeItem.

Why: A single email typically contains multiple swaps, each with multiple legs. Each leg becomes a separate TradeItem. The wrapper allows returning all items from one extraction pass.

Alternative considered: Returning List<TradeItem> directly. Rejected because a wrapper object is more extensible (could add metadata like extraction confidence, email subject, etc. later) and produces cleaner JSON ({"items": [...]} vs bare array).

2. TradeItem uses snake_case JSON property names

Decision: Use [JsonPropertyName("valuedate")] etc. to produce snake_case JSON output.

Why: The existing external APIs and downstream consumers expect snake_case. The C# properties will use PascalCase per convention, with JSON attributes for serialization.

3. One plugin class with multiple methods vs multiple plugin classes

Decision: Single ExtractionPlugin class with 3-5 [KernelFunction] methods.

Why: All tools serve the same extraction workflow. SK discovers functions by class, so a single class keeps registration simple (ImportPluginFromObject). If tools grow beyond 6-7, split into focused plugin classes.

Alternative considered: Separate plugin class per external API. Rejected as premature — adds registration complexity for no benefit at 3-5 methods.

4. External API calls via typed HttpClients

Decision: Register typed HttpClient instances for each external API via AddHttpClient<T>() in Program.cs. Each plugin method receives its client via constructor injection.

Why: Typed HttpClients are the standard ASP.NET Core pattern. They support per-client base URL configuration, DI, and are easily mockable in tests.

5. Disambiguation via tool return values, not special UI

Decision: When a tool like lookup_counterparty finds multiple matches, it returns the candidate list as JSON. The LLM agent sees the candidates and asks the user to choose via natural language in the streamed response.

Why: This uses the existing streaming conversation infrastructure with zero client changes. The agent is already conversational — it simply says "I found 3 matches, which one?" and the user replies.

Alternative considered: Structured disambiguation UI (radio buttons, dropdowns). Rejected for this change — adds client complexity and a new response type. Can be added later as a UX enhancement.

6. ExtractionPlugin conditionally loaded per request

Decision: Continue importing extraction plugins per-request in the controller (_kernel.ImportPluginFromObject), not at startup.

Why: General chat requests don't need extraction tools. Per-request import keeps the tool list clean for non-extraction conversations, reducing token usage and avoiding confusing the LLM with irrelevant tools.

Risks / Trade-offs

[External API availability] → Plugin methods should handle HTTP errors gracefully and return structured error responses that the LLM can reason about (e.g., "Counterparty API unavailable, proceeding without legal entity lookup"). The agent can then inform the user.

[Breaking change to ExtractedFields] → Any code referencing the old schema breaks. Mitigation: this is a small codebase with known consumers. Update all references in the same change. Tests will catch missed spots.

[LLM disambiguation quality] → The agent must correctly interpret candidate lists and present clear choices. Mitigation: tool descriptions must be explicit about what the return values mean. Few-shot examples (next change) will reinforce the pattern.

[External API response format coupling] → Plugin methods parse external API responses. If those APIs change, plugins break. Mitigation: typed response DTOs with defensive deserialization. External APIs are stable and owned by the same team.