Files
AgenticCode/openspec/exports/chat-agent-full-openspec.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

132 lines
8.2 KiB
Markdown

# OpenSpec Artifacts for: AI Chat Agent with Streaming & Rich Text
On the target machine with OpenSpec + Claude Code:
1. Create `openspec/config.yaml` (see below)
2. Save `proposal.md` to `openspec/changes/chat-agent-full/proposal.md`
3. Save `design.md` to `openspec/changes/chat-agent-full/design.md`
4. Save `tasks.md` to `openspec/changes/chat-agent-full/tasks.md`
5. Run `/opsx:apply chat-agent-full`
6. Use the portable spec (`chat-agent-full-spec.md`) as reference if the AI needs detail
---
## config.yaml
Adapt to your project name and constraints, save as `openspec/config.yaml`:
```yaml
schema: spec-driven
context: |
<Your Project Name> — <one-line description>.
Tech stack: .NET 9, C# 13, Blazor WASM, ASP.NET Core Web API.
Key libraries: MudBlazor 9.2.0, Semantic Kernel 1.74.0, Markdig 1.1.1.
<Any target-specific constraints>.
```
---
## proposal.md
**Integration Rule**: This feature is additive only. DO NOT modify existing files, components, services, or patterns. If the target already has an equivalent service (HTTP wrapper, markdown renderer, etc.), use the existing one. If a task conflicts with existing code, stop and notify the user before proceeding. Existing applicationX code takes precedence in all cases.
Build a complete AI chat interface on an existing MudBlazor app:
- Chat page with streaming AI responses via SSE
- Backend using Semantic Kernel with tool calling (extraction plugin)
- Multi-turn conversation support
- Rich text rendering (Markdig + HTML sanitization)
- Full xUnit test coverage
Assumes MudBlazor is already installed and configured.
---
## design.md
### Integration: additive only
- This feature is a GUEST in the target application — add new files, never modify existing ones
- If the target already has a service that overlaps (e.g., its own HttpClient wrapper, markdown renderer, error handling), use theirs — do not create a duplicate
- Conform to the target's code style, naming conventions, and DI patterns
- If a task would require modifying existing code, **stop and notify the user** — the user decides whether to skip, adapt, or redesign
- Only touch existing files to add a nav link or a DI registration line — never restructure
### Streaming: SSE over SignalR
- SSE is simpler — one-directional text stream over HTTP, no WebSocket negotiation
- Blazor WASM supports streaming via `SetBrowserResponseStreamingEnabled(true)` + `ResponseHeadersRead`
- Client parses line-by-line: `data: {"text":"..."}\n\n` for deltas, `data: [DONE]\n\n` for end, `data: {"error":"..."}` for errors
- **CRITICAL SSE loop pattern** — use `while ((line = await reader.ReadLineAsync()) != null)`. Do NOT use `reader.EndOfStream` — it does a synchronous peek that Blazor WASM's fetch-backed stream rejects at runtime
### AI orchestration: Semantic Kernel
- SK provides chat completion connectors, plugin system, and auto function calling
- OpenAI connector works with any OpenAI-compatible endpoint (e.g. local proxy) — base URL **must** include `/v1`
- ExtractionPlugin imported per-request via `ImportPluginFromObject` (not at Kernel build, avoids plugin state leaking across requests)
- `FunctionChoiceBehavior.Auto()` lets the LLM autonomously call tools and retry
### Client architecture
- **Typed HttpClient** (`ChatApiClient`) — centralizes API paths, easy to mock for tests, IHttpClientFactory manages handler lifetime
- **MarkdownService** (singleton) — Markdig pipeline is immutable/thread-safe; two-pass sanitization (regex strip script/style, then tag allowlist) rather than a sanitization library to keep dependencies minimal
- **Rendered HTML cache** — `Dictionary<ChatMessage, string>` prevents re-running Markdig on all completed messages during every `StateHasChanged` while streaming
### Chat UI layout — "Sales Assistant" page
- Page renders **inside MudMainContent** — do NOT add a separate AppBar or layout
- Route: `/sales-assistant`, add MudNavLink in existing sidebar NavMenu
- Flexbox column: message-list (flex 1, scrollable) + input-area (pinned bottom)
- MudPaper bubbles: user right-aligned (primary bg), assistant left-aligned (surface bg)
- `::deep` CSS selectors needed for MudBlazor child components and MarkupString-injected HTML
- Auto-scroll via JS interop (`scrollTop = scrollHeight`) — no built-in Blazor scroll API
- **Container height**: `calc(100vh - 64px)` — CRC app uses regular MudAppBar (~64px), not Dense. MudDrawer width is handled by MudLayout automatically — no horizontal calc needed
### Multi-turn
- Full conversation history sent with each request (all messages with non-empty content)
- Empty assistant placeholder excluded to avoid confusing the LLM
### Test strategy
- API: `WebApplicationFactory<Program>` with mocked `IChatCompletionService` — tests SSE contract without hitting real LLM
- Client: mock `HttpMessageHandler` with canned SSE response streams
- Requires `public partial class Program { }` in API for test factory access
---
## tasks.md
### Phase 1: Shared Models
- [ ] Create `ChatMessage.cs` in Shared/Models — Role (string), Content (string), Timestamp (DateTime)
- [ ] Create `ChatRequest.cs` — List<ChatMessage> Messages
- [ ] Create `HealthResponse.cs` — Status (string), Timestamp (DateTime)
- [ ] Create `ExtractedFields.cs` — Client?, Project?, Hours?, Rate?, Currency?, Date? (required); Description?, PoNumber? (optional)
- [ ] Create `ValidationResult.cs` — IsValid (bool), Errors (List<string>)
### Phase 2: API Backend
- [ ] Add NuGet: Microsoft.SemanticKernel 1.74.0, Connectors.OpenAI 1.74.0
- [ ] Create `ExtractionPlugin.cs` in Plugins/ — [KernelFunction] validates extracted fields JSON
- [ ] Configure Program.cs: AddControllers, AddOpenAIChatCompletion (base URL must include /v1), AddKernel, ExtractionPlugin singleton, CORS for client origin
- [ ] Create `HealthController.cs` — GET /api/health returns HealthResponse
- [ ] Create `ChatController.cs` — POST /api/chat, streams via SK GetStreamingChatMessageContentsAsync, SSE format: data: {"text":"..."}\n\n and data: [DONE]\n\n
- [ ] Add `public partial class Program { }` for test accessibility
- [ ] Add appsettings.json: ResponsesApi:BaseUrl, ResponsesApi:Model
### Phase 3: Client Services
- [ ] Add NuGet: Microsoft.Extensions.Http 9.0.4, Markdig 1.1.1
- [ ] Create `ChatApiClient.cs` — typed HttpClient with GetHealthAsync and SendChatStreamingAsync (IAsyncEnumerable<string>). Use SetBrowserResponseStreamingEnabled(true) + ResponseHeadersRead. Parse SSE with ReadLineAsync null check (not EndOfStream).
- [ ] Create `MarkdownService.cs` — Markdig with UseAdvancedExtensions, two-pass HTML sanitization (script/style strip, tag allowlist)
- [ ] Register in Program.cs: AddMudServices, AddSingleton<MarkdownService>, AddHttpClient<ChatApiClient>
- [ ] Add wwwroot/appsettings.json with ApiBaseUrl_Http and ApiBaseUrl_Https
### Phase 4: Chat UI
- [ ] Create `SalesAssistant.razor` at route "/sales-assistant" — message list with MudPaper bubbles, MudTextField with send icon, Enter key handler
- [ ] Add MudNavLink to existing sidebar NavMenu: "Sales Assistant", icon SmartToy, href /sales-assistant
- [ ] Streaming: append tokens to assistant message, StateHasChanged per token, auto-scroll via JS interop
- [ ] Assistant messages: render as (MarkupString) from MarkdownService inside .markdown-body div, with rendered HTML cache
- [ ] User messages: plain MudText
- [ ] Thinking indicator: MudProgressCircular when assistant content empty during streaming
- [ ] New Chat button: clears messages, visible when conversation exists, disabled during streaming
- [ ] Multi-turn: send all non-empty messages with each request
- [ ] Create `SalesAssistant.razor.css` — flex layout with `calc(100vh - 64px)` height (64px = regular AppBar), message bubbles (user right/primary, assistant left/surface), markdown styles (code blocks, tables, blockquotes, headings, links)
### Phase 5: Tests
- [ ] Create xUnit test project for API with WebApplicationFactory, mock IChatCompletionService
- [ ] Test HealthController (200 + valid response), ChatController (SSE streaming, error handling), ExtractionPlugin (validation logic)
- [ ] Create xUnit test project for Client with mock HttpMessageHandler
- [ ] Test ChatApiClient (delta parsing, error events), MarkdownService (rendering + sanitization)
- [ ] Verify: `dotnet test`