feat: migrate chat backend to Semantic Kernel with tool calling support

Replace manual HTTP proxy in ChatController with Semantic Kernel's
OpenAI chat completion service pointed at CLIProxyAPI. Add extraction
plugin with validation function for structured field extraction from
natural language, enabling an agentic loop with auto-retry and
human-in-the-loop escalation.

- Add Microsoft.SemanticKernel 1.74.0 with OpenAI connector
- Create ExtractedFields schema and ValidationResult models
- Create ExtractionPlugin with [KernelFunction] validation
- Rewrite ChatController to use IChatCompletionService streaming
- Configure FunctionChoiceBehavior.Auto() for tool calling
- Preserve existing SSE contract (client unchanged)
- Update tests to mock SK services, add plugin and integration tests
- Archive multi-turn-conversations and migrate-to-semantic-kernel changes
- Sync specs for agent-extraction, semantic-kernel-integration, chat-streaming

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
local
2026-04-04 23:59:13 +01:00
parent 3278a408b9
commit 471e9ce935
27 changed files with 1082 additions and 201 deletions

View File

@@ -0,0 +1,70 @@
## Purpose
Define the autonomous agent-driven extraction pipeline — structured field extraction from natural language, schema-based validation via tool calling, autonomous retry logic, and human-in-the-loop clarification.
## Requirements
### Requirement: Structured field extraction from natural language
The agent SHALL extract a predefined set of key-value pairs from user-provided natural language text (e.g., email content) and return them as a structured JSON object.
#### Scenario: All fields extracted successfully
- **WHEN** the user sends a message containing natural language with all required information
- **THEN** the agent returns a JSON object with all predefined fields populated from the text
#### Scenario: Partial extraction
- **WHEN** the user sends a message that contains some but not all required fields
- **THEN** the agent extracts available fields and leaves missing fields as null
### Requirement: Predefined extraction schema
The system SHALL define a fixed set of known field names and types as a strongly-typed C# class. All extraction output MUST conform to this schema.
#### Scenario: Output conforms to schema
- **WHEN** the agent produces extracted fields
- **THEN** every key in the output matches a field defined in the schema and values match expected types
### Requirement: Autonomous validation via tool calling
The agent SHALL validate extracted fields by calling a validation tool function. The validation tool checks that all required fields are present and correctly typed.
#### Scenario: Validation passes
- **WHEN** the agent calls the validation tool with a complete and correct extraction
- **THEN** the tool returns a success result and the agent returns the final output to the user
#### Scenario: Validation fails with fixable errors
- **WHEN** the validation tool returns errors for missing or malformed fields
- **THEN** the agent re-reads the source text and attempts to fix the extraction without user intervention
### Requirement: Autonomous retry with iteration cap
The agent SHALL retry extraction autonomously up to 3 times when validation fails. After exhausting retries, the agent MUST escalate to the user.
#### Scenario: Agent retries and succeeds
- **WHEN** validation fails on the first attempt but the error is recoverable
- **THEN** the agent retries extraction and calls validation again, up to 3 total attempts
#### Scenario: Agent exhausts retries and escalates
- **WHEN** validation fails after 3 attempts
- **THEN** the agent sends a natural language message to the user identifying the specific fields it could not resolve and asking for clarification
### Requirement: Human-in-the-loop clarification
When the agent escalates to the user, the user SHALL be able to provide the missing information in natural language, and the agent SHALL incorporate the clarification and re-attempt extraction.
#### Scenario: User provides clarification
- **WHEN** the agent asks for clarification about missing fields and the user responds
- **THEN** the agent incorporates the user's response into the conversation context and produces an updated extraction
#### Scenario: Clarification via normal chat
- **WHEN** the agent escalates for clarification
- **THEN** the clarification request appears as a regular assistant message in the chat UI, and the user responds via the normal chat input

View File

@@ -1,40 +1,40 @@
## Purpose
Define the streaming AI response pipeline — backend proxy to the Responses API, SSE delivery to the WASM client, configuration, and error handling.
Define the streaming AI response pipeline — backend chat endpoint using Semantic Kernel, SSE delivery to the WASM client, configuration, and error handling.
## Requirements
### Requirement: Chat endpoint proxies to Responses API
The API backend SHALL expose `POST /api/chat` that accepts a list of messages and proxies the request to the local Responses API at a configurable base URL using the `POST /v1/responses` endpoint.
The API backend SHALL expose `POST /api/chat` that accepts a list of messages and processes them using a Semantic Kernel chat completion service. The kernel is configured with an OpenAI connector pointed at the existing CLIProxyAPI proxy.
#### Scenario: Successful proxy request
#### Scenario: Successful chat request
- **WHEN** the client sends a POST to `/api/chat` with a message list
- **THEN** the API forwards the messages to the Responses API with the configured model and returns the response
- **THEN** the API processes the messages through the Semantic Kernel and returns the response
### Requirement: Streaming response delivery
The API backend SHALL stream the Responses API's SSE events back to the WASM client as `text/event-stream`, forwarding `response.output_text.delta` events so the client can render tokens incrementally.
The API backend SHALL stream the Semantic Kernel's chat completion response back to the WASM client as `text/event-stream`, forwarding text content so the client can render tokens incrementally. The SSE event format MUST remain `data: {"text":"..."}\n\n` for text deltas and `data: [DONE]\n\n` for completion.
#### Scenario: Tokens stream to client
- **WHEN** the Responses API emits `response.output_text.delta` events
- **THEN** the backend forwards each delta as an SSE event to the client containing the text fragment
- **WHEN** the Semantic Kernel emits streaming chat message content
- **THEN** the backend forwards each content chunk as an SSE event to the client containing the text fragment
#### Scenario: Stream completes
- **WHEN** the Responses API emits `response.completed`
- **THEN** the backend signals stream completion to the client
- **WHEN** the Semantic Kernel streaming response completes
- **THEN** the backend signals stream completion to the client with `data: [DONE]\n\n`
### Requirement: Configurable proxy target
The Responses API base URL and model name SHALL be configurable via `appsettings.json` in the API project, not hardcoded.
The CLIProxyAPI base URL and model name SHALL be configurable via `appsettings.json` in the API project, not hardcoded. These values are used to configure the Semantic Kernel OpenAI connector.
#### Scenario: Configuration read at startup
- **WHEN** the API starts
- **THEN** it reads `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from configuration
- **THEN** it reads `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from configuration to configure the Semantic Kernel
### Requirement: Client streams from backend
@@ -47,9 +47,9 @@ The WASM client SHALL call `POST /api/chat` with `SetBrowserResponseStreamingEna
### Requirement: Error propagation
If the Responses API returns an error or is unreachable, the API backend SHALL return an appropriate HTTP error status and the client SHALL display the error to the user.
If the LLM service returns an error or is unreachable, the API backend SHALL return an error SSE event and the client SHALL display the error to the user.
#### Scenario: Proxy unreachable
#### Scenario: LLM service unreachable
- **WHEN** the Responses API is not running
- **WHEN** the CLIProxyAPI proxy is not running
- **THEN** the client displays an error message instead of an assistant response

View File

@@ -63,13 +63,32 @@ The chat page SHALL show a visual indicator while waiting for the first token fr
### Requirement: Streaming AI response
The assistant SHALL reply with a real AI response streamed from the backend API. Tokens appear incrementally as they arrive.
The assistant SHALL reply with a real AI response streamed from the backend API, using the full conversation history as context. Tokens appear incrementally as they arrive.
#### Scenario: Bot replies with streamed AI response
- **WHEN** the user sends any message
- **THEN** the assistant message appears and grows token by token as the stream delivers text
#### Scenario: Full history sent with each request
- **WHEN** the user sends a message after prior exchanges
- **THEN** all previous user and assistant messages are included in the API request so the AI has conversational context
### Requirement: New chat button
The chat page SHALL provide a button to clear the current conversation and start a new one.
#### Scenario: User starts a new chat
- **WHEN** the user clicks the "New Chat" button
- **THEN** all messages are cleared and the empty state is shown
#### Scenario: New chat button disabled during streaming
- **WHEN** the assistant is currently streaming a response
- **THEN** the "New Chat" button is disabled
### Requirement: Auto-scroll
The message list SHALL automatically scroll to the newest message when a new message is added.

View File

@@ -0,0 +1,46 @@
## Purpose
Define the Semantic Kernel integration layer — kernel registration, OpenAI connector configuration, plugin registration, and automatic function calling.
## Requirements
### Requirement: Semantic Kernel service registration
The API backend SHALL register a Semantic Kernel `Kernel` instance in the ASP.NET Core DI container at startup, configured with an OpenAI chat completion connector.
#### Scenario: Kernel registered at startup
- **WHEN** the API application starts
- **THEN** a `Kernel` instance is available for injection into controllers
### Requirement: OpenAI connector targets CLIProxyAPI proxy
The Semantic Kernel OpenAI chat completion service SHALL be configured to use the existing CLIProxyAPI proxy endpoint as its base URL, reading the URL and model name from `appsettings.json`.
#### Scenario: Connector uses configured endpoint
- **WHEN** the kernel makes a chat completion request
- **THEN** it sends the request to the URL specified in `ResponsesApi:BaseUrl` configuration
#### Scenario: Model from configuration
- **WHEN** the kernel makes a chat completion request
- **THEN** it uses the model name specified in `ResponsesApi:Model` configuration
### Requirement: Plugin registration
The API backend SHALL register extraction and validation plugins with the Kernel so they are available as tools for the LLM to invoke.
#### Scenario: Plugins available as tools
- **WHEN** the kernel is constructed
- **THEN** all registered plugin functions appear in the tool list sent to the LLM
### Requirement: Auto function calling
The Kernel SHALL be configured with automatic function calling enabled, allowing the LLM to invoke registered plugin functions without manual dispatch code.
#### Scenario: LLM invokes tool automatically
- **WHEN** the LLM decides to call a registered function during chat completion
- **THEN** the kernel automatically executes the function and returns the result to the LLM